Merge main: take the poster tape's overlap rule, keep the touch minimum
Their tape system supersedes mine and is the better invariant, so it wins whole. I had made both stickers self-centre on the card's edge; they pin both by --card-pad-top and --tape-overlap, which makes the OVERLAP onto the banner the constant instead of the centre. That is what the Driver's rule actually asks for -- extra height then grows into empty space rather than walking over the artwork -- and one variable now governs both stickers so neither can drift from the other. Kept only the orthogonal part of mine: the (pointer:coarse) block that lifts .pbtn, .pstock and .tape-h to the 24px WCAG 2.5.8 minimum. Dropped my padding override on .tape-h -- their rule sizes and places that sticker, and re-stating its padding here would fight a geometry the Driver has already approved. Also took their new .pstock.is-on state. Fixed a comment of mine that the merge made false: it claimed both tapes self-centre, which stopped being true in this very commit. Rebuilt all 23 bundles, not just the ones I touched. The incoming diff brought library/i18n/index.js and three shared library components, which reach nearly every bundle -- and regenerating the tree without rebuilding has shipped a stale .js here once before. tree + fix:sri after; check:sri PASS across 54 script references on 55 leaf pages.
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His paths straight.
|
||||
|
||||
Isaiah 40:3 “...Prepare the way for the LORD in the wilderness; make a straight highway for our God in the desert."
|
||||
**Isaiah 40:3 “...Prepare the way for the LORD in the wilderness; make a straight highway for our God in the desert."**
|
||||
|
||||
# dx-maps — one build, maps that embed everywhere
|
||||
|
||||
|
|
@ -209,8 +209,8 @@ iframe at it.
|
|||
<summary><b>More about method 2 — mobile app</b></summary>
|
||||
|
||||
[Capacitor](https://capacitorjs.com) wraps the built maps into an Android app — the same files,
|
||||
no new code. Shown here: the UPG-100 prayer lists and posters, one poster per row, with print
|
||||
options that fit a phone.
|
||||
no new code. Shown here: the UPG-100 prayer lists and posters at phone size, each poster with its
|
||||
own print options.
|
||||
|
||||
```bash
|
||||
bun run check:mobile # checks the app is wired up correctly — needs no Android tools
|
||||
|
|
|
|||
10541
data/people-groups/doxa-groups-snapshot.json
Normal file
1
data/people-groups/people-groups-points.json
Normal file
40
data/people-groups/vision-funnel.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"sources": {
|
||||
"imb": {
|
||||
"file": "data/people-groups/people_groups.csv",
|
||||
"publisher": "IMB / peoplegroups.org",
|
||||
"captured": "2026-07-16"
|
||||
},
|
||||
"doxa": {
|
||||
"file": "data/people-groups/doxa-groups-snapshot.json",
|
||||
"source": "https://pray.doxa.life/api/people-groups/list",
|
||||
"fetched": "2026-09-25"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "all",
|
||||
"groups": 12379,
|
||||
"population": 8288563969
|
||||
},
|
||||
{
|
||||
"id": "unreached",
|
||||
"groups": 7341,
|
||||
"population": 4893057899
|
||||
},
|
||||
{
|
||||
"id": "frontier",
|
||||
"groups": 4754,
|
||||
"population": 2813855464
|
||||
},
|
||||
{
|
||||
"id": "doxa",
|
||||
"groups": 2107,
|
||||
"population": 209079159,
|
||||
"frontier": 1745,
|
||||
"unreached": 2082,
|
||||
"matched": 2091,
|
||||
"unmatched": 16
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -70,8 +70,8 @@
|
|||
"title": "The gap by country",
|
||||
"grain": "one row per country that has at least one people group on this map",
|
||||
"generated": "2026-09-25",
|
||||
"measured": "242 countries · 1,613 groups with nothing found · 207,867,995 people",
|
||||
"by_region": "Oceania 30.1% (399 of 1325) · Africa 14.2% (438 of 3074) · Asia 11.3% (575 of 5073) · Europe 8.9% (70 of 790) · Americas 6.2% (131 of 2117)",
|
||||
"measured": "242 countries · 1,592 groups with nothing found · 205,861,745 people",
|
||||
"by_region": "Oceania 30.1% (399 of 1325) · Africa 14.1% (433 of 3074) · Asia 11.0% (560 of 5073) · Europe 8.9% (70 of 790) · Americas 6.1% (130 of 2117)",
|
||||
"three_questions": "Groups with nothing, people with nothing, and the share of the country each represents. They rank differently on purpose — Papua New Guinea has the most groups and few people, China the reverse, and Oceania has the worst share of any region while being the smallest by population.",
|
||||
"source": "data/tables/gospel-by-country.json, which is itself refreshed from data/tables/people-group-pins.json — the same index the map draws, so this table and the map cannot disagree.",
|
||||
"curated_columns": "growing_unreached and deaths_day come from the curated country analysis and are null where it does not cover the country. Null, never zero: zero is a claim this build cannot make."
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
"iso3": "CHN",
|
||||
"region": "Asia",
|
||||
"people_groups": 391,
|
||||
"groups_with_nothing": 83,
|
||||
"pct_with_nothing": 21.2,
|
||||
"people_with_nothing": 127348300,
|
||||
"groups_with_nothing": 76,
|
||||
"pct_with_nothing": 19.4,
|
||||
"people_with_nothing": 125832700,
|
||||
"groups_with_scripture": 118,
|
||||
"groups_with_film": 227,
|
||||
"band": "partial",
|
||||
|
|
@ -182,21 +182,6 @@
|
|||
"deaths_day": 44,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"region": "Asia",
|
||||
"people_groups": 158,
|
||||
"groups_with_nothing": 45,
|
||||
"pct_with_nothing": 28.5,
|
||||
"people_with_nothing": 692685,
|
||||
"groups_with_scripture": 55,
|
||||
"groups_with_film": 58,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 15.5,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vanuatu",
|
||||
"iso3": "VUT",
|
||||
|
|
@ -212,21 +197,6 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"region": "Africa",
|
||||
"people_groups": 185,
|
||||
"groups_with_nothing": 42,
|
||||
"pct_with_nothing": 22.7,
|
||||
"people_with_nothing": 2414100,
|
||||
"groups_with_scripture": 96,
|
||||
"groups_with_film": 117,
|
||||
"band": "partial",
|
||||
"growing_unreached": 36,
|
||||
"deaths_day": 359.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Chad",
|
||||
"iso3": "TCD",
|
||||
|
|
@ -242,6 +212,36 @@
|
|||
"deaths_day": 68.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"region": "Africa",
|
||||
"people_groups": 185,
|
||||
"groups_with_nothing": 39,
|
||||
"pct_with_nothing": 21.1,
|
||||
"people_with_nothing": 2280200,
|
||||
"groups_with_scripture": 96,
|
||||
"groups_with_film": 117,
|
||||
"band": "partial",
|
||||
"growing_unreached": 36,
|
||||
"deaths_day": 359.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"region": "Asia",
|
||||
"people_groups": 158,
|
||||
"groups_with_nothing": 38,
|
||||
"pct_with_nothing": 24.1,
|
||||
"people_with_nothing": 553985,
|
||||
"groups_with_scripture": 55,
|
||||
"groups_with_film": 58,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 15.5,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Brazil",
|
||||
"iso3": "BRA",
|
||||
|
|
@ -287,21 +287,6 @@
|
|||
"deaths_day": 0.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "United States of America",
|
||||
"iso3": "USA",
|
||||
"region": "Americas",
|
||||
"people_groups": 623,
|
||||
"groups_with_nothing": 18,
|
||||
"pct_with_nothing": 2.9,
|
||||
"people_with_nothing": 322725,
|
||||
"groups_with_scripture": 517,
|
||||
"groups_with_film": 549,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vietnam",
|
||||
"iso3": "VNM",
|
||||
|
|
@ -317,6 +302,21 @@
|
|||
"deaths_day": 5.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "United States of America",
|
||||
"iso3": "USA",
|
||||
"region": "Americas",
|
||||
"people_groups": 623,
|
||||
"groups_with_nothing": 17,
|
||||
"pct_with_nothing": 2.7,
|
||||
"people_with_nothing": 285225,
|
||||
"groups_with_scripture": 517,
|
||||
"groups_with_film": 549,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Russian Federation",
|
||||
"iso3": "RUS",
|
||||
|
|
@ -902,21 +902,6 @@
|
|||
"deaths_day": 2.8,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Guinea",
|
||||
"iso3": "GIN",
|
||||
"region": "Africa",
|
||||
"people_groups": 41,
|
||||
"groups_with_nothing": 4,
|
||||
"pct_with_nothing": 9.8,
|
||||
"people_with_nothing": 146600,
|
||||
"groups_with_scripture": 19,
|
||||
"groups_with_film": 23,
|
||||
"band": "partial",
|
||||
"growing_unreached": 7,
|
||||
"deaths_day": 14.5,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Equatorial Guinea",
|
||||
"iso3": "GNQ",
|
||||
|
|
@ -1082,6 +1067,21 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Guinea",
|
||||
"iso3": "GIN",
|
||||
"region": "Africa",
|
||||
"people_groups": 41,
|
||||
"groups_with_nothing": 3,
|
||||
"pct_with_nothing": 7.3,
|
||||
"people_with_nothing": 139000,
|
||||
"groups_with_scripture": 19,
|
||||
"groups_with_film": 23,
|
||||
"band": "partial",
|
||||
"growing_unreached": 7,
|
||||
"deaths_day": 14.5,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Eritrea",
|
||||
"iso3": "ERI",
|
||||
|
|
@ -1112,21 +1112,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Sierra Leone",
|
||||
"iso3": "SLE",
|
||||
"region": "Africa",
|
||||
"people_groups": 28,
|
||||
"groups_with_nothing": 3,
|
||||
"pct_with_nothing": 10.7,
|
||||
"people_with_nothing": 98950,
|
||||
"groups_with_scripture": 21,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Norway",
|
||||
"iso3": "NOR",
|
||||
|
|
@ -1232,21 +1217,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
"region": "Asia",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 2,
|
||||
"pct_with_nothing": 4.4,
|
||||
"people_with_nothing": 594000,
|
||||
"groups_with_scripture": 15,
|
||||
"groups_with_film": 25,
|
||||
"band": "partial",
|
||||
"growing_unreached": 17,
|
||||
"deaths_day": 17.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Uganda",
|
||||
"iso3": "UGA",
|
||||
|
|
@ -1427,6 +1397,21 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Sierra Leone",
|
||||
"iso3": "SLE",
|
||||
"region": "Africa",
|
||||
"people_groups": 28,
|
||||
"groups_with_nothing": 2,
|
||||
"pct_with_nothing": 7.1,
|
||||
"people_with_nothing": 98000,
|
||||
"groups_with_scripture": 21,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Nicaragua",
|
||||
"iso3": "NIC",
|
||||
|
|
@ -1772,6 +1757,21 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
"region": "Asia",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 1,
|
||||
"pct_with_nothing": 2.2,
|
||||
"people_with_nothing": 422000,
|
||||
"groups_with_scripture": 15,
|
||||
"groups_with_film": 25,
|
||||
"band": "partial",
|
||||
"growing_unreached": 17,
|
||||
"deaths_day": 17.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Poland",
|
||||
"iso3": "POL",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"generated": "2026-09-25",
|
||||
"groups": 2107,
|
||||
"by_access": {
|
||||
"nothing found": 381,
|
||||
"watch or hear, cannot read": 794,
|
||||
"nothing found": 369,
|
||||
"watch or hear, cannot read": 806,
|
||||
"read and watch": 869,
|
||||
"read": 57,
|
||||
"stories only": 6
|
||||
|
|
@ -1001,15 +1001,15 @@
|
|||
"iso639_3": "alk",
|
||||
"population": 25500,
|
||||
"religion": "Ethnoreligion - Animism",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/alk",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/alak?source=doxalife",
|
||||
"people_praying": 2,
|
||||
|
|
@ -1208,15 +1208,15 @@
|
|||
"iso639_3": "nos",
|
||||
"population": 9300,
|
||||
"religion": "Ethnoreligion - Chinese Folk Religion",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/nos",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/alu?source=doxalife",
|
||||
"people_praying": 3,
|
||||
|
|
@ -2588,15 +2588,15 @@
|
|||
"iso639_3": "jle",
|
||||
"population": 2400,
|
||||
"religion": "Islam - Sunni",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/jle",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/arra?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -8568,15 +8568,15 @@
|
|||
"iso639_3": "nos",
|
||||
"population": 10500,
|
||||
"religion": "Ethnoreligion - Chinese Folk Religion",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/nos",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/chesu?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -13651,15 +13651,15 @@
|
|||
"iso639_3": "jle",
|
||||
"population": 14500,
|
||||
"religion": "Islam - Sunni",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/jle",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/doloka?source=doxalife",
|
||||
"people_praying": 1,
|
||||
|
|
@ -14387,15 +14387,15 @@
|
|||
"iso639_3": "lma",
|
||||
"population": 7500,
|
||||
"religion": "Ethnoreligion - Animism",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/lma",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/east-limba?source=doxalife",
|
||||
"people_praying": 1,
|
||||
|
|
@ -23541,15 +23541,15 @@
|
|||
"iso639_3": "ngt",
|
||||
"population": 15000,
|
||||
"religion": "Ethnoreligion - Animism",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/ngt",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/khlor?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -26692,15 +26692,15 @@
|
|||
"iso639_3": "nos",
|
||||
"population": 10500,
|
||||
"religion": "Ethnoreligion - Chinese Folk Religion",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/nos",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/laowu?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -26876,15 +26876,15 @@
|
|||
"iso639_3": "lbo",
|
||||
"population": 56500,
|
||||
"religion": "Ethnoreligion - Animism",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/lbo",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/laven?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -33247,15 +33247,15 @@
|
|||
"iso639_3": "jle",
|
||||
"population": 117000,
|
||||
"religion": "Islam - Sunni",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/jle",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/ngile?source=doxalife",
|
||||
"people_praying": 0,
|
||||
|
|
@ -33845,15 +33845,15 @@
|
|||
"iso639_3": "nev",
|
||||
"population": 11000,
|
||||
"religion": "Ethnoreligion - Animism",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/nev",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/nyahon?source=doxalife",
|
||||
"people_praying": 1,
|
||||
|
|
@ -41527,15 +41527,15 @@
|
|||
"iso639_3": "psi",
|
||||
"population": 172000,
|
||||
"religion": "Islam - Sunni",
|
||||
"access": "nothing found",
|
||||
"resources": 0,
|
||||
"access": "watch or hear, cannot read",
|
||||
"resources": 1,
|
||||
"scripture": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
"gospel_audio_url": "",
|
||||
"gospel_audio_url": "https://fivefish.org/psi",
|
||||
"training_url": "",
|
||||
"pray_url": "https://pray.doxa.life/southeast-pashayi?source=doxalife",
|
||||
"people_praying": 1,
|
||||
|
|
|
|||
|
|
@ -943,7 +943,7 @@
|
|||
"lng": 106.59719,
|
||||
"iso639_3": "alk",
|
||||
"language": "Alak",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -1159,7 +1159,7 @@
|
|||
"lng": 102.744099,
|
||||
"iso639_3": "nos",
|
||||
"language": "Nisu, Eastern",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -2599,7 +2599,7 @@
|
|||
"lng": 30.00524,
|
||||
"iso639_3": "jle",
|
||||
"language": "Ngile",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -8839,7 +8839,7 @@
|
|||
"lng": 101.821229,
|
||||
"iso639_3": "nos",
|
||||
"language": "Nisu, Eastern",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -14143,7 +14143,7 @@
|
|||
"lng": 29.748111,
|
||||
"iso639_3": "jle",
|
||||
"language": "Ngile",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -14911,7 +14911,7 @@
|
|||
"lng": -11.73884,
|
||||
"iso639_3": "lma",
|
||||
"language": "Limba, East",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -23903,7 +23903,7 @@
|
|||
"religion": "Hinduism",
|
||||
"population": 166000,
|
||||
"people_praying": 0,
|
||||
"people_committed": 1,
|
||||
"people_committed": 2,
|
||||
"adopted_by_churches": 0,
|
||||
"mapped": true,
|
||||
"matched_by": "doxa detail",
|
||||
|
|
@ -24239,7 +24239,7 @@
|
|||
"religion": "Hinduism",
|
||||
"population": 11500,
|
||||
"people_praying": 2,
|
||||
"people_committed": 3,
|
||||
"people_committed": 4,
|
||||
"adopted_by_churches": 0,
|
||||
"mapped": true,
|
||||
"matched_by": "doxa detail",
|
||||
|
|
@ -24463,7 +24463,7 @@
|
|||
"lng": 106.5484,
|
||||
"iso639_3": "ngt",
|
||||
"language": "Kriang",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 2,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -27751,7 +27751,7 @@
|
|||
"lng": 102.7273,
|
||||
"iso639_3": "nos",
|
||||
"language": "Nisu, Eastern",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -27943,7 +27943,7 @@
|
|||
"lng": 106.32196,
|
||||
"iso639_3": "lbo",
|
||||
"language": "Laven",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -34591,7 +34591,7 @@
|
|||
"lng": 29.835037,
|
||||
"iso639_3": "jle",
|
||||
"language": "Ngile",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -35215,7 +35215,7 @@
|
|||
"lng": 106.54295,
|
||||
"iso639_3": "nev",
|
||||
"language": "Nyaheun",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
@ -39575,7 +39575,7 @@
|
|||
"religion": "Islam - Sunni",
|
||||
"population": 86000,
|
||||
"people_praying": 2,
|
||||
"people_committed": 2,
|
||||
"people_committed": 1,
|
||||
"adopted_by_churches": 0,
|
||||
"mapped": true,
|
||||
"matched_by": "doxa detail",
|
||||
|
|
@ -43231,7 +43231,7 @@
|
|||
"lng": 70.449337,
|
||||
"iso639_3": "psi",
|
||||
"language": "Pashai, Southeast",
|
||||
"tier": 0,
|
||||
"tier": 1,
|
||||
"imb_total_resources": 1,
|
||||
"engagement": "Unengaged"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9983,8 +9983,8 @@
|
|||
"population": 172000,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Dari",
|
||||
"bridge_iso639_3": "prs",
|
||||
"bridge_groups_in_country": 2,
|
||||
|
|
@ -9997,7 +9997,7 @@
|
|||
"top_channel": "WhatsApp Messenger",
|
||||
"channel_source": "app-store ranking",
|
||||
"aligned": false,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -11647,8 +11647,8 @@
|
|||
"population": 117000,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Arabic, Sudanese",
|
||||
"bridge_iso639_3": "apd",
|
||||
"bridge_groups_in_country": 65,
|
||||
|
|
@ -11661,7 +11661,7 @@
|
|||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": false,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -15677,8 +15677,8 @@
|
|||
"population": 56500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Akha",
|
||||
"bridge_iso639_3": "ahk",
|
||||
"bridge_groups_in_country": 12,
|
||||
|
|
@ -15690,8 +15690,8 @@
|
|||
"internet_pct": 65.6,
|
||||
"top_channel": "WhatsApp Messenger",
|
||||
"channel_source": "app-store ranking",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -22099,8 +22099,8 @@
|
|||
"population": 25500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Akha",
|
||||
"bridge_iso639_3": "ahk",
|
||||
"bridge_groups_in_country": 12,
|
||||
|
|
@ -22112,8 +22112,8 @@
|
|||
"internet_pct": 65.6,
|
||||
"top_channel": "WhatsApp Messenger",
|
||||
"channel_source": "app-store ranking",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -26545,8 +26545,8 @@
|
|||
"population": 15000,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Akha",
|
||||
"bridge_iso639_3": "ahk",
|
||||
"bridge_groups_in_country": 12,
|
||||
|
|
@ -26558,8 +26558,8 @@
|
|||
"internet_pct": 65.6,
|
||||
"top_channel": "WhatsApp Messenger",
|
||||
"channel_source": "app-store ranking",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -26831,8 +26831,8 @@
|
|||
"population": 14500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Arabic, Sudanese",
|
||||
"bridge_iso639_3": "apd",
|
||||
"bridge_groups_in_country": 65,
|
||||
|
|
@ -26845,7 +26845,7 @@
|
|||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": false,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -29067,8 +29067,8 @@
|
|||
"population": 11000,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Akha",
|
||||
"bridge_iso639_3": "ahk",
|
||||
"bridge_groups_in_country": 12,
|
||||
|
|
@ -29080,8 +29080,8 @@
|
|||
"internet_pct": 65.6,
|
||||
"top_channel": "WhatsApp Messenger",
|
||||
"channel_source": "app-store ranking",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -29171,8 +29171,8 @@
|
|||
"population": 10500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Chinese, Mandarin",
|
||||
"bridge_iso639_3": "cmn",
|
||||
"bridge_groups_in_country": 25,
|
||||
|
|
@ -29184,8 +29184,8 @@
|
|||
"internet_pct": 92,
|
||||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -29327,8 +29327,8 @@
|
|||
"population": 10500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Chinese, Mandarin",
|
||||
"bridge_iso639_3": "cmn",
|
||||
"bridge_groups_in_country": 25,
|
||||
|
|
@ -29340,8 +29340,8 @@
|
|||
"internet_pct": 92,
|
||||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -30315,8 +30315,8 @@
|
|||
"population": 9300,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Chinese, Mandarin",
|
||||
"bridge_iso639_3": "cmn",
|
||||
"bridge_groups_in_country": 25,
|
||||
|
|
@ -30328,8 +30328,8 @@
|
|||
"internet_pct": 92,
|
||||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -31953,8 +31953,8 @@
|
|||
"population": 7500,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Pulaar",
|
||||
"bridge_iso639_3": "fuc",
|
||||
"bridge_groups_in_country": 2,
|
||||
|
|
@ -31966,8 +31966,8 @@
|
|||
"internet_pct": 33.3,
|
||||
"top_channel": "Facebook",
|
||||
"channel_source": "referral share",
|
||||
"aligned": true,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"aligned": false,
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
@ -40689,8 +40689,8 @@
|
|||
"population": 2400,
|
||||
"heart_has_scripture": false,
|
||||
"heart_has_film": false,
|
||||
"heart_has_any": false,
|
||||
"opportunity": true,
|
||||
"heart_has_any": true,
|
||||
"opportunity": false,
|
||||
"bridge_language": "Arabic, Sudanese",
|
||||
"bridge_iso639_3": "apd",
|
||||
"bridge_groups_in_country": 65,
|
||||
|
|
@ -40703,7 +40703,7 @@
|
|||
"top_channel": "Twitter",
|
||||
"channel_source": "referral share",
|
||||
"aligned": false,
|
||||
"status": "opportunity — bridge language has a resource",
|
||||
"status": "reached in heart language",
|
||||
"caveat": "Bridge = a language of the same country spoken by >1 DX group. NOT proof this group speaks it. Resource status = pools checked (eBible + JESUS Film); audio pools are key-blocked."
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@
|
|||
"country": "People's Republic of China",
|
||||
"region": "Asia",
|
||||
"groups": 391,
|
||||
"no_resource": 83,
|
||||
"pop_no_resource": 127348300,
|
||||
"no_resource": 76,
|
||||
"pop_no_resource": 125832700,
|
||||
"scripture": 118,
|
||||
"film": 227,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 274.2,
|
||||
"growth": -0.12,
|
||||
"pct_no_resource": 21.2,
|
||||
"pct_no_resource": 19.4,
|
||||
"band": "partial",
|
||||
"curated_groups": 158
|
||||
},
|
||||
|
|
@ -30,14 +30,14 @@
|
|||
"country": "Lao People's Democratic Republic",
|
||||
"region": "Asia",
|
||||
"groups": 158,
|
||||
"no_resource": 45,
|
||||
"pop_no_resource": 692685,
|
||||
"no_resource": 38,
|
||||
"pop_no_resource": 553985,
|
||||
"scripture": 55,
|
||||
"film": 58,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 15.5,
|
||||
"growth": null,
|
||||
"pct_no_resource": 28.5,
|
||||
"pct_no_resource": 24.1,
|
||||
"band": "partial",
|
||||
"curated_groups": 90
|
||||
},
|
||||
|
|
@ -94,14 +94,14 @@
|
|||
"country": "Sudan",
|
||||
"region": "Africa",
|
||||
"groups": 185,
|
||||
"no_resource": 42,
|
||||
"pop_no_resource": 2414100,
|
||||
"no_resource": 39,
|
||||
"pop_no_resource": 2280200,
|
||||
"scripture": 96,
|
||||
"film": 117,
|
||||
"growing_unreached": 36,
|
||||
"deaths_day": 359.6,
|
||||
"growth": 0.81,
|
||||
"pct_no_resource": 22.7,
|
||||
"pct_no_resource": 21.1,
|
||||
"band": "partial",
|
||||
"curated_groups": 105
|
||||
},
|
||||
|
|
@ -142,14 +142,14 @@
|
|||
"country": "Afghanistan",
|
||||
"region": "Asia",
|
||||
"groups": 45,
|
||||
"no_resource": 2,
|
||||
"pop_no_resource": 594000,
|
||||
"no_resource": 1,
|
||||
"pop_no_resource": 422000,
|
||||
"scripture": 15,
|
||||
"film": 25,
|
||||
"growing_unreached": 17,
|
||||
"deaths_day": 17.6,
|
||||
"growth": 2.84,
|
||||
"pct_no_resource": 4.4,
|
||||
"pct_no_resource": 2.2,
|
||||
"band": "partial",
|
||||
"curated_groups": 21
|
||||
},
|
||||
|
|
@ -334,14 +334,14 @@
|
|||
"country": "Guinea",
|
||||
"region": "Africa",
|
||||
"groups": 41,
|
||||
"no_resource": 4,
|
||||
"pop_no_resource": 146600,
|
||||
"no_resource": 3,
|
||||
"pop_no_resource": 139000,
|
||||
"scripture": 19,
|
||||
"film": 23,
|
||||
"growing_unreached": 7,
|
||||
"deaths_day": 14.5,
|
||||
"growth": 2.4,
|
||||
"pct_no_resource": 9.8,
|
||||
"pct_no_resource": 7.3,
|
||||
"band": "partial",
|
||||
"curated_groups": 13
|
||||
},
|
||||
|
|
@ -2238,14 +2238,14 @@
|
|||
"country": "Sierra Leone",
|
||||
"region": "Africa",
|
||||
"groups": 28,
|
||||
"no_resource": 3,
|
||||
"pop_no_resource": 98950,
|
||||
"no_resource": 2,
|
||||
"pop_no_resource": 98000,
|
||||
"scripture": 21,
|
||||
"film": 24,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 1.1,
|
||||
"growth": 2.12,
|
||||
"pct_no_resource": 10.7,
|
||||
"pct_no_resource": 7.1,
|
||||
"band": "partial",
|
||||
"curated_groups": 2
|
||||
},
|
||||
|
|
@ -2414,14 +2414,14 @@
|
|||
"country": "United States of America",
|
||||
"region": "Americas",
|
||||
"groups": 623,
|
||||
"no_resource": 18,
|
||||
"pop_no_resource": 322725,
|
||||
"no_resource": 17,
|
||||
"pop_no_resource": 285225,
|
||||
"scripture": 517,
|
||||
"film": 549,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0.1,
|
||||
"growth": 0.96,
|
||||
"pct_no_resource": 2.9,
|
||||
"pct_no_resource": 2.7,
|
||||
"band": "partial",
|
||||
"curated_groups": 3
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"source": "https://fivefish.org/<iso639-3> (5fish, Global Recordings Network)",
|
||||
"fetched": "2026-09-25",
|
||||
"_note": "GRN gospel AUDIO, typed as Gospel audio and never as Scripture — \"Words of Life\" is gospel messages and Bible stories, not a translation of the canon. The host never 404s: a nonsense path returns a geo-located page with HTTP 200, so existence is read from the page title, with a negative and a positive control run in band every time. Their robots.txt asks for Crawl-delay: 10 and this fetcher obeys it, sequentially.",
|
||||
"codes": 1022,
|
||||
"decidedNone": 1325,
|
||||
"codes": 1030,
|
||||
"decidedNone": 1314,
|
||||
"byIso": {
|
||||
"mnp": {
|
||||
"u": "https://fivefish.org/mnp",
|
||||
|
|
@ -4092,6 +4092,38 @@
|
|||
"ora": {
|
||||
"u": "https://fivefish.org/ora",
|
||||
"n": "Oroha"
|
||||
},
|
||||
"nos": {
|
||||
"u": "https://fivefish.org/nos",
|
||||
"n": "Nisu, Eastern"
|
||||
},
|
||||
"jle": {
|
||||
"u": "https://fivefish.org/jle",
|
||||
"n": "Ngile"
|
||||
},
|
||||
"alk": {
|
||||
"u": "https://fivefish.org/alk",
|
||||
"n": "Alak"
|
||||
},
|
||||
"lma": {
|
||||
"u": "https://fivefish.org/lma",
|
||||
"n": "Limba, East"
|
||||
},
|
||||
"psi": {
|
||||
"u": "https://fivefish.org/psi",
|
||||
"n": "Pashai, Southeast"
|
||||
},
|
||||
"lbo": {
|
||||
"u": "https://fivefish.org/lbo",
|
||||
"n": "Laven"
|
||||
},
|
||||
"ngt": {
|
||||
"u": "https://fivefish.org/ngt",
|
||||
"n": "Kriang"
|
||||
},
|
||||
"nev": {
|
||||
"u": "https://fivefish.org/nev",
|
||||
"n": "Nyaheun"
|
||||
}
|
||||
},
|
||||
"none": [
|
||||
|
|
@ -4104,7 +4136,6 @@
|
|||
"nsi",
|
||||
"zlj",
|
||||
"acx",
|
||||
"nos",
|
||||
"qcd",
|
||||
"zgb",
|
||||
"eth",
|
||||
|
|
@ -4135,7 +4166,6 @@
|
|||
"asp",
|
||||
"vic",
|
||||
"aed",
|
||||
"qns",
|
||||
"han",
|
||||
"qiq",
|
||||
"mry",
|
||||
|
|
@ -4200,7 +4230,6 @@
|
|||
"dux",
|
||||
"ajz",
|
||||
"mdi",
|
||||
"psi",
|
||||
"bxu",
|
||||
"nrb",
|
||||
"cda",
|
||||
|
|
@ -4223,7 +4252,6 @@
|
|||
"dpp",
|
||||
"muq",
|
||||
"tbi",
|
||||
"jle",
|
||||
"buc",
|
||||
"qfa",
|
||||
"tse",
|
||||
|
|
@ -4253,7 +4281,6 @@
|
|||
"sqm",
|
||||
"bdr",
|
||||
"bct",
|
||||
"lbo",
|
||||
"mei",
|
||||
"bfl",
|
||||
"sgg",
|
||||
|
|
@ -4404,13 +4431,11 @@
|
|||
"tey",
|
||||
"kcu",
|
||||
"biw",
|
||||
"ngt",
|
||||
"ymc",
|
||||
"baw",
|
||||
"tlu",
|
||||
"gey",
|
||||
"ttd",
|
||||
"alk",
|
||||
"kko",
|
||||
"xdq",
|
||||
"gdi",
|
||||
|
|
@ -4622,7 +4647,6 @@
|
|||
"put",
|
||||
"wha",
|
||||
"ije",
|
||||
"nev",
|
||||
"acy",
|
||||
"jpr",
|
||||
"phh",
|
||||
|
|
@ -4674,7 +4698,6 @@
|
|||
"mcm",
|
||||
"bml",
|
||||
"aoa",
|
||||
"lma",
|
||||
"ykn",
|
||||
"tgu",
|
||||
"qgy",
|
||||
|
|
@ -5125,7 +5148,6 @@
|
|||
"gmu",
|
||||
"rea",
|
||||
"nee",
|
||||
"qki",
|
||||
"wnb",
|
||||
"bcd",
|
||||
"yee",
|
||||
|
|
@ -5295,7 +5317,6 @@
|
|||
"wab",
|
||||
"trb",
|
||||
"jur",
|
||||
"xor",
|
||||
"dbe",
|
||||
"tad",
|
||||
"lce",
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@
|
|||
"has_scripture": false,
|
||||
"full_bible": false,
|
||||
"has_film": false,
|
||||
"carries_resource": false,
|
||||
"carries_resource": true,
|
||||
"country_people_groups": 154
|
||||
},
|
||||
{
|
||||
|
|
@ -479,7 +479,7 @@
|
|||
"has_scripture": false,
|
||||
"full_bible": false,
|
||||
"has_film": false,
|
||||
"carries_resource": false,
|
||||
"carries_resource": true,
|
||||
"country_people_groups": 105
|
||||
},
|
||||
{
|
||||
|
|
@ -2504,7 +2504,7 @@
|
|||
"has_scripture": false,
|
||||
"full_bible": false,
|
||||
"has_film": false,
|
||||
"carries_resource": false,
|
||||
"carries_resource": true,
|
||||
"country_people_groups": 21
|
||||
},
|
||||
{
|
||||
|
|
@ -3959,7 +3959,7 @@
|
|||
"has_scripture": false,
|
||||
"full_bible": false,
|
||||
"has_film": false,
|
||||
"carries_resource": false,
|
||||
"carries_resource": true,
|
||||
"country_people_groups": 13
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1643,8 +1643,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -1979,8 +1979,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2003,8 +2003,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2027,8 +2027,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -3443,8 +3443,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -5435,8 +5435,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -5459,8 +5459,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -5483,8 +5483,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -6203,8 +6203,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -7835,8 +7835,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -8987,8 +8987,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -9923,8 +9923,8 @@
|
|||
"has_audio": "unknown",
|
||||
"has_open_bible_stories": false,
|
||||
"obs_note": "",
|
||||
"no_resource_found": true,
|
||||
"any_resource_found": false,
|
||||
"no_resource_found": false,
|
||||
"any_resource_found": true,
|
||||
"scripture_url": "",
|
||||
"film_url": "",
|
||||
"code_unmatchable": false,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"source": "data/tables/translation-priority.json, itself built from the pin index — so this table, that one and the map cannot disagree.",
|
||||
"generated": "2026-09-25",
|
||||
"countries": 159,
|
||||
"groups": 3115,
|
||||
"people": 1330905094,
|
||||
"groups": 3136,
|
||||
"people": 1332911344,
|
||||
"definition": "People groups with recorded audio, the JESUS Film, a Bible video or Words of Life in their language and NO text Scripture found in the pools checked, aggregated to the country a team would be sent to. \"Not found\" is a statement about those pools, never a claim that no text exists; the per-group table carries the checked and unchecked pools on every row.",
|
||||
"two_columns_note": "groups and languages answer different questions from people. Nigeria and the DRC lead on languages, which is the translation workload; China and Pakistan lead on population across far fewer languages. Neither is ranked for you."
|
||||
},
|
||||
|
|
@ -55,9 +55,9 @@
|
|||
{
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"groups": 189,
|
||||
"languages": 89,
|
||||
"people": 218383850,
|
||||
"groups": 196,
|
||||
"languages": 90,
|
||||
"people": 219899450,
|
||||
"largest_group": "Han Chinese, Wu",
|
||||
"largest_group_population": 92530000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
|
|
@ -155,9 +155,9 @@
|
|||
{
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
"groups": 28,
|
||||
"languages": 28,
|
||||
"people": 26167200,
|
||||
"groups": 29,
|
||||
"languages": 29,
|
||||
"people": 26339200,
|
||||
"largest_group": "Pashtun, Southern",
|
||||
"largest_group_population": 13680000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
|
|
@ -262,6 +262,16 @@
|
|||
"largest_group_population": 3035000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"groups": 48,
|
||||
"languages": 39,
|
||||
"people": 8892975,
|
||||
"largest_group": "Beja",
|
||||
"largest_group_population": 3220000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Senegal",
|
||||
"iso3": "SEN",
|
||||
|
|
@ -272,16 +282,6 @@
|
|||
"largest_group_population": 2545000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"groups": 45,
|
||||
"languages": 38,
|
||||
"people": 8759075,
|
||||
"largest_group": "Beja",
|
||||
"largest_group_population": 3220000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Cote d'Ivoire",
|
||||
"iso3": "CIV",
|
||||
|
|
@ -335,9 +335,9 @@
|
|||
{
|
||||
"country": "Guinea",
|
||||
"iso3": "GIN",
|
||||
"groups": 18,
|
||||
"languages": 15,
|
||||
"people": 5947500,
|
||||
"groups": 19,
|
||||
"languages": 16,
|
||||
"people": 5955100,
|
||||
"largest_group": "Maninka",
|
||||
"largest_group_population": 3855000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
|
|
@ -425,9 +425,9 @@
|
|||
{
|
||||
"country": "United States of America",
|
||||
"iso3": "USA",
|
||||
"groups": 79,
|
||||
"languages": 76,
|
||||
"people": 3530175,
|
||||
"groups": 80,
|
||||
"languages": 77,
|
||||
"people": 3567675,
|
||||
"largest_group": "Armenian",
|
||||
"largest_group_population": 1180000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
|
|
@ -582,6 +582,16 @@
|
|||
"largest_group_population": 1220000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"groups": 65,
|
||||
"languages": 38,
|
||||
"people": 2186025,
|
||||
"largest_group": "White Thai",
|
||||
"largest_group_population": 325000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "JOR",
|
||||
"iso3": "JOR",
|
||||
|
|
@ -612,16 +622,6 @@
|
|||
"largest_group_population": 225000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"groups": 58,
|
||||
"languages": 33,
|
||||
"people": 2047325,
|
||||
"largest_group": "White Thai",
|
||||
"largest_group_population": 325000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
},
|
||||
{
|
||||
"country": "KOS",
|
||||
"iso3": "KOS",
|
||||
|
|
@ -915,9 +915,9 @@
|
|||
{
|
||||
"country": "Sierra Leone",
|
||||
"iso3": "SLE",
|
||||
"groups": 4,
|
||||
"languages": 4,
|
||||
"people": 532000,
|
||||
"groups": 5,
|
||||
"languages": 5,
|
||||
"people": 532950,
|
||||
"largest_group": "Sherbro",
|
||||
"largest_group_population": 244000,
|
||||
"basis": "no text Scripture found in the pools checked"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
"title": "Translation priority — can hear it, no text found",
|
||||
"source": "data/tables/people-group-pins.json — the same index the map draws, so this table and the map cannot disagree.",
|
||||
"generated": "2026-09-25",
|
||||
"groups": 3115,
|
||||
"languages": 1900,
|
||||
"people": 1330905094,
|
||||
"groups": 3136,
|
||||
"languages": 1908,
|
||||
"people": 1332911344,
|
||||
"definition": "A people group with recorded audio, the JESUS Film, a Bible video or Words of Life in their language, and NO text Scripture found in the pools checked. \"Not found\" is a statement about those pools, never a claim that no text exists — several of the largest rows are varieties where a recording plainly exists and a text in that same variety is contested. The checked and unchecked pools are on every row for that reason."
|
||||
},
|
||||
"columns": [
|
||||
|
|
@ -2128,6 +2128,20 @@
|
|||
"lat": 24.69,
|
||||
"lng": 46.66
|
||||
},
|
||||
{
|
||||
"people_group": "Nisu Yi",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 1430000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 23.99,
|
||||
"lng": 102.12
|
||||
},
|
||||
{
|
||||
"people_group": "Bulu",
|
||||
"country": "Cameroon",
|
||||
|
|
@ -9786,6 +9800,20 @@
|
|||
"lat": -5.76,
|
||||
"lng": 123.91
|
||||
},
|
||||
{
|
||||
"people_group": "Pashayi, Southeast",
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
"language": "psi",
|
||||
"iso639_3": "psi",
|
||||
"population": 172000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/psi",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 34.42,
|
||||
"lng": 70.45
|
||||
},
|
||||
{
|
||||
"people_group": "Gha-Mu",
|
||||
"country": "People's Republic of China",
|
||||
|
|
@ -11970,6 +11998,20 @@
|
|||
"lat": 12.13,
|
||||
"lng": -2.98
|
||||
},
|
||||
{
|
||||
"people_group": "Ngile",
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"language": "Ngile",
|
||||
"iso639_3": "jle",
|
||||
"population": 117000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/jle",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 10.91,
|
||||
"lng": 29.84
|
||||
},
|
||||
{
|
||||
"people_group": "Burji",
|
||||
"country": "Ethiopia",
|
||||
|
|
@ -16394,6 +16436,20 @@
|
|||
"lat": 9.35,
|
||||
"lng": 11.05
|
||||
},
|
||||
{
|
||||
"people_group": "Laven",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Laven",
|
||||
"iso639_3": "lbo",
|
||||
"population": 56500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/lbo",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.27,
|
||||
"lng": 106.32
|
||||
},
|
||||
{
|
||||
"people_group": "Numana",
|
||||
"country": "Nigeria",
|
||||
|
|
@ -19404,6 +19460,20 @@
|
|||
"lat": 29.2,
|
||||
"lng": 83.95
|
||||
},
|
||||
{
|
||||
"people_group": "Boloven",
|
||||
"country": "United States of America",
|
||||
"iso3": "USA",
|
||||
"language": "Laven",
|
||||
"iso639_3": "lbo",
|
||||
"population": 37500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/lbo",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 36.72,
|
||||
"lng": -119.73
|
||||
},
|
||||
{
|
||||
"people_group": "Polci",
|
||||
"country": "Nigeria",
|
||||
|
|
@ -21336,6 +21406,20 @@
|
|||
"lat": -6.18,
|
||||
"lng": 14.36
|
||||
},
|
||||
{
|
||||
"people_group": "Boka",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 28500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 23.17,
|
||||
"lng": 103.87
|
||||
},
|
||||
{
|
||||
"people_group": "Fuyuge",
|
||||
"country": "Papua New Guinea",
|
||||
|
|
@ -22078,6 +22162,20 @@
|
|||
"lat": 12.84,
|
||||
"lng": 75.25
|
||||
},
|
||||
{
|
||||
"people_group": "Alak",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Alak",
|
||||
"iso639_3": "alk",
|
||||
"population": 25500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/alk",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.56,
|
||||
"lng": 106.6
|
||||
},
|
||||
{
|
||||
"people_group": "Jakun",
|
||||
"country": "Malaysia",
|
||||
|
|
@ -22386,6 +22484,20 @@
|
|||
"lat": 9.48,
|
||||
"lng": -5.64
|
||||
},
|
||||
{
|
||||
"people_group": "Lesu",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 24500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 23.89,
|
||||
"lng": 102.14
|
||||
},
|
||||
{
|
||||
"people_group": "Pangwala",
|
||||
"country": "India",
|
||||
|
|
@ -24626,6 +24738,20 @@
|
|||
"lat": 27.98,
|
||||
"lng": 96.62
|
||||
},
|
||||
{
|
||||
"people_group": "Ngae",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Kriang",
|
||||
"iso639_3": "ngt",
|
||||
"population": 17500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/ngt",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.52,
|
||||
"lng": 106.75
|
||||
},
|
||||
{
|
||||
"people_group": "Omo",
|
||||
"country": "Papua New Guinea",
|
||||
|
|
@ -25732,6 +25858,20 @@
|
|||
"lat": -6.67,
|
||||
"lng": 146.66
|
||||
},
|
||||
{
|
||||
"people_group": "Khlor",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Kriang",
|
||||
"iso639_3": "ngt",
|
||||
"population": 15000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/ngt",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.68,
|
||||
"lng": 106.55
|
||||
},
|
||||
{
|
||||
"people_group": "Tai Nua",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
|
|
@ -26068,6 +26208,20 @@
|
|||
"lat": 17.69,
|
||||
"lng": 120.66
|
||||
},
|
||||
{
|
||||
"people_group": "Doloka",
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"language": "Ngile",
|
||||
"iso639_3": "jle",
|
||||
"population": 14500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/jle",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 10.86,
|
||||
"lng": 29.75
|
||||
},
|
||||
{
|
||||
"people_group": "Shui, Yunnan",
|
||||
"country": "People's Republic of China",
|
||||
|
|
@ -27762,6 +27916,20 @@
|
|||
"lat": 9.92,
|
||||
"lng": 12.53
|
||||
},
|
||||
{
|
||||
"people_group": "Laowu",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 11000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 22.74,
|
||||
"lng": 102.73
|
||||
},
|
||||
{
|
||||
"people_group": "Southern Efate",
|
||||
"country": "Vanuatu",
|
||||
|
|
@ -27972,6 +28140,20 @@
|
|||
"lat": 10.65,
|
||||
"lng": -5.41
|
||||
},
|
||||
{
|
||||
"people_group": "Nyahon",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Nyaheun",
|
||||
"iso639_3": "nev",
|
||||
"population": 11000,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nev",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.1,
|
||||
"lng": 106.54
|
||||
},
|
||||
{
|
||||
"people_group": "Sayyid (Kashmiri)",
|
||||
"country": "India",
|
||||
|
|
@ -28476,6 +28658,20 @@
|
|||
"lat": -22.27,
|
||||
"lng": 166.41
|
||||
},
|
||||
{
|
||||
"people_group": "Chesu",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 10500,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 24.26,
|
||||
"lng": 101.82
|
||||
},
|
||||
{
|
||||
"people_group": "Igo",
|
||||
"country": "Togo",
|
||||
|
|
@ -29064,6 +29260,20 @@
|
|||
"lat": -6.05,
|
||||
"lng": 149.34
|
||||
},
|
||||
{
|
||||
"people_group": "Alu",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 9400,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 22.88,
|
||||
"lng": 102.74
|
||||
},
|
||||
{
|
||||
"people_group": "Ndasa",
|
||||
"country": "Republic of the Congo",
|
||||
|
|
@ -29918,6 +30128,20 @@
|
|||
"lat": 7.11,
|
||||
"lng": -67.6
|
||||
},
|
||||
{
|
||||
"people_group": "Alu",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 8100,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 22.26,
|
||||
"lng": 101.68
|
||||
},
|
||||
{
|
||||
"people_group": "Sivandi",
|
||||
"country": "Islamic Republic of Iran",
|
||||
|
|
@ -30226,6 +30450,20 @@
|
|||
"lat": -1.75,
|
||||
"lng": 135.87
|
||||
},
|
||||
{
|
||||
"people_group": "East Limba",
|
||||
"country": "Guinea",
|
||||
"iso3": "GIN",
|
||||
"language": "Limba, East",
|
||||
"iso639_3": "lma",
|
||||
"population": 7600,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/lma",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 10.04,
|
||||
"lng": -11.74
|
||||
},
|
||||
{
|
||||
"people_group": "Kalash",
|
||||
"country": "Pakistan",
|
||||
|
|
@ -32690,6 +32928,20 @@
|
|||
"lat": 48.85,
|
||||
"lng": 2.31
|
||||
},
|
||||
{
|
||||
"people_group": "Alak Sanak",
|
||||
"country": "Lao People's Democratic Republic",
|
||||
"iso3": "LAO",
|
||||
"language": "Alak",
|
||||
"iso639_3": "alk",
|
||||
"population": 5100,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/alk",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 15.47,
|
||||
"lng": 106.94
|
||||
},
|
||||
{
|
||||
"people_group": "Langbashe",
|
||||
"country": "Democratic Republic of the Congo",
|
||||
|
|
@ -36400,6 +36652,20 @@
|
|||
"lat": 17.89,
|
||||
"lng": 105.49
|
||||
},
|
||||
{
|
||||
"people_group": "Arra",
|
||||
"country": "Sudan",
|
||||
"iso3": "SDN",
|
||||
"language": "Ngile",
|
||||
"iso639_3": "jle",
|
||||
"population": 2400,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/jle",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 10.64,
|
||||
"lng": 30.01
|
||||
},
|
||||
{
|
||||
"people_group": "Jerung",
|
||||
"country": "Nepal",
|
||||
|
|
@ -38234,6 +38500,20 @@
|
|||
"lat": 51.43,
|
||||
"lng": -0.21
|
||||
},
|
||||
{
|
||||
"people_group": "Azong",
|
||||
"country": "People's Republic of China",
|
||||
"iso3": "CHN",
|
||||
"language": "Nisu, Eastern",
|
||||
"iso639_3": "nos",
|
||||
"population": 1700,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/nos",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 22.74,
|
||||
"lng": 101.98
|
||||
},
|
||||
{
|
||||
"people_group": "Onabasulu",
|
||||
"country": "Papua New Guinea",
|
||||
|
|
@ -40152,6 +40432,20 @@
|
|||
"lat": -13.25,
|
||||
"lng": 166.61
|
||||
},
|
||||
{
|
||||
"people_group": "East Limba",
|
||||
"country": "Sierra Leone",
|
||||
"iso3": "SLE",
|
||||
"language": "Limba, East",
|
||||
"iso639_3": "lma",
|
||||
"population": 950,
|
||||
"can_hear": "gospel audio (Words of Life)",
|
||||
"first_link": "https://fivefish.org/lma",
|
||||
"checked_pools": "arclight, door43, ebible, fcbh, grn, scriptureearth",
|
||||
"unchecked_pools": "api_bible, dbl, progress_bible, youversion",
|
||||
"lat": 9.38,
|
||||
"lng": -11.76
|
||||
},
|
||||
{
|
||||
"people_group": "Doupo",
|
||||
"country": "People's Republic of China",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 892 KiB After Width: | Height: | Size: 828 KiB |
|
|
@ -1,35 +1,35 @@
|
|||
{
|
||||
"cap1-single-element": {
|
||||
"bundle": "all-people-groups",
|
||||
"sha256": "db9e293144928477bc7fc1ef50f469db073d81575b29ba394b50d33273c6e1a5"
|
||||
"sha256": "312b3a0f64b0842b498ae8c0c88491a9225c353a60c0420e05ac18fa0333a0a5"
|
||||
},
|
||||
"cap2-adoption": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap2-engagement": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap2-prayer": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap3-monolithic-research": {
|
||||
"bundle": "doxa-research-map",
|
||||
"sha256": "56845dc5bd33198a7dd8fb42895700f21b4bc1c3b7f98f57832e8373c7782a83"
|
||||
"sha256": "7e3637a635faf8223488a4bc21c1f308c5b3ea94fa6c80d5f1560a04ef7ec9cb"
|
||||
},
|
||||
"cap4-upg100-list": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap4-upg100-mfe": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap4-upg100-poster-wall": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap5-doxa-maps-spa": {
|
||||
"bundle": "doxa-maps-spa",
|
||||
|
|
@ -41,6 +41,6 @@
|
|||
},
|
||||
"deploy2-mobile-upg100": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
|
||||
> declares tiers 3/4/5.
|
||||
|
||||
Generated from 22 bundle(s).
|
||||
Generated from 21 bundle(s).
|
||||
|
||||
### One canonical exemplar per tier (generated — quote these)
|
||||
|
||||
|
|
@ -52,8 +52,6 @@ Generated from 22 bundle(s).
|
|||
| `prayer-walk-planner` | `prayer-walk-planner` | `single` |
|
||||
| `regions-dashboard` | `region-dashboards` | `single` |
|
||||
| `regions-dashboard` | `regions-list` | `single` |
|
||||
| `template-bundle` | `template-map-a` | `single` |
|
||||
| `template-bundle` | `template-map-b` | `single` |
|
||||
|
||||
## Capability 2 — preset-element (prop-table)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
*
|
||||
* NEGATIVE-TESTED: restoring either label ('% unreached' or 'Adopted %') fails by name with exit 1.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
|
|
@ -41,10 +41,22 @@ const src = readFileSync(CFG, 'utf8')
|
|||
// Strip comments before scanning. This file now DOCUMENTS the defect in a comment right above the
|
||||
// fixed line, and a naive scan matches that prose and fails on the explanation of the fix — the
|
||||
// same self-reference that has bitten three gates in this repo already.
|
||||
// The translated labels live in the catalogue, so the rule has to reach them too.
|
||||
const LOC_DIR = resolve(ROOT, 'library/i18n/locales')
|
||||
const LOCALES = existsSync(LOC_DIR) ? readdirSync(LOC_DIR) : []
|
||||
const catalogue = (loc) => {
|
||||
try { return JSON.parse(readFileSync(resolve(LOC_DIR, loc, 'dashboard.json'), 'utf8')) } catch { return {} }
|
||||
}
|
||||
|
||||
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
||||
|
||||
// Each KPI entry is a single-line-ish object literal: { id: '…', label: '…', kind: '…', … }
|
||||
const entries = [...code.matchAll(/\{[^{}]*\bid:\s*'([^']+)'[^{}]*\}/g)]
|
||||
// Each KPI entry is an object literal: { id: '…', label: '…', kind: '…', … }.
|
||||
// ★ IT MAY NOW CONTAIN ONE LEVEL OF NESTED BRACES. The labels became GETTERS when the dashboard was
|
||||
// translated (`get label() { return tStatic('dashboard.unreached') || 'unreached' }`), and the old
|
||||
// single-brace pattern stopped matching every entry — the gate found ZERO pct KPIs and failed
|
||||
// exactly as it was built to, rather than printing a green PASS about nothing. That refusal is the
|
||||
// gate working; this is the parse catching up with the config, not a relaxation of the rule.
|
||||
const entries = [...code.matchAll(/\{(?:[^{}]|\{[^{}]*\})*\bid:\s*'([^']+)'(?:[^{}]|\{[^{}]*\})*\}/g)]
|
||||
const offenders = []
|
||||
let pctSeen = 0
|
||||
|
||||
|
|
@ -53,8 +65,20 @@ for (const m of entries) {
|
|||
const kind = (body.match(/\bkind:\s*'([^']+)'/) || [])[1]
|
||||
if (kind !== 'pct') continue
|
||||
pctSeen++
|
||||
const label = (body.match(/\blabel:\s*'([^']*)'/) || [])[1]
|
||||
// Two label forms: a plain literal, or a getter that falls back to one.
|
||||
const plain = (body.match(/\blabel:\s*'([^']*)'/) || [])[1]
|
||||
const viaGetter = body.match(/get label\(\)\s*\{[^}]*?tStatic\('([^']+)'\)\s*\|\|\s*'([^']*)'/)
|
||||
const label = plain ?? (viaGetter ? viaGetter[2] : undefined)
|
||||
if (label && label.includes('%')) offenders.push({ id: m[1], label })
|
||||
// ★ AND CHECK WHAT A NON-ENGLISH READER SEES. The rule is about the rendered label carrying a
|
||||
// second percent sign, and a translation can reintroduce one where the English fallback has none.
|
||||
if (viaGetter) {
|
||||
const key = viaGetter[1].replace(/^dashboard\./, '')
|
||||
for (const loc of LOCALES) {
|
||||
const v = catalogue(loc)[key]
|
||||
if (typeof v === 'string' && v.includes('%')) offenders.push({ id: m[1] + ' [' + loc + ']', label: v })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pctSeen) {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,40 @@ const SUPPORTED = (src.match(/SUPPORTED_LOCALES = \[([^\]]*)\]/) || [])[1]
|
|||
|
||||
// Evaluate the SHIPPED detectLocale rather than reimplementing it — a gate that
|
||||
// reimplements the thing it tests only ever proves the copy agrees with itself.
|
||||
const body = src.slice(src.indexOf('export function detectLocale'), src.indexOf('export function createAppI18n'))
|
||||
// ★ TAKE THE FUNCTION, NOT THE GAP BETWEEN TWO LANDMARKS.
|
||||
// This used to slice from `export function detectLocale` to `export function createAppI18n` and eval
|
||||
// whatever lay between. That is a promise about the ORDER OF A FILE the gate does not own: the day
|
||||
// someone exported a helper between those two lines, the slice carried a bare `export` into
|
||||
// new Function() and this gate died with "Unexpected token 'export'" — twice, once for a helper I
|
||||
// added myself. It was then "fixed" by moving the helper, which only re-armed the trap for the next
|
||||
// person. Extract detectLocale by matching its own braces, and strip any export keyword that
|
||||
// survives, so the gate reads the function it names and nothing else.
|
||||
function extractFunction(source, name) {
|
||||
const start = source.search(new RegExp('(export\\s+)?function\\s+' + name + '\\s*\\('))
|
||||
if (start < 0) return ''
|
||||
const open = source.indexOf('{', start)
|
||||
if (open < 0) return ''
|
||||
let depth = 0
|
||||
for (let i = open; i < source.length; i++) {
|
||||
const ch = source[i]
|
||||
if (ch === '{') depth++
|
||||
else if (ch === '}') {
|
||||
depth--
|
||||
if (depth === 0) return source.slice(start, i + 1)
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const body = extractFunction(src, 'detectLocale').replace(/\bexport\s+/g, '')
|
||||
if (!body) {
|
||||
console.error('LOCALE-REACHABLE: FAIL — could not find detectLocale() in library/i18n/index.js.')
|
||||
console.error(' The gate evaluates the SHIPPED function rather than reimplementing it, so if the')
|
||||
console.error(' function is renamed this must be updated — silence here would be a gate that')
|
||||
console.error(' stopped testing anything.')
|
||||
process.exit(1)
|
||||
}
|
||||
const make = new Function('SUPPORTED_LOCALES', 'window', 'document',
|
||||
`${body.replace('export function', 'return function')}\n; return detectLocale;`)
|
||||
`${body}\n; return detectLocale;`)
|
||||
const detect = (search, htmlLang) =>
|
||||
make(SUPPORTED, { location: { search } }, { documentElement: { lang: htmlLang } })()
|
||||
|
||||
|
|
|
|||
152
internal/check-map-embed-cells.mjs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-map-embed-cells — measure the PRODUCTION-MATRIX embed cells B (iframe) and C (script embed)
|
||||
* for each production map, on a genuinely FOREIGN origin: the built tree on 127.0.0.1:<port>, a
|
||||
* stand-in partner page on localhost:<other port>. Starts both servers itself; needs a built tree.
|
||||
*
|
||||
* USAGE: node internal/check-map-embed-cells.mjs [--only bundle/profile,bundle/profile] [--cell B|C]
|
||||
* Graded by EXIT CODE: 0 all measured cells pass · 1 a cell failed · 2 nothing could be measured.
|
||||
*
|
||||
* B · iframe: the partner page iframes the map's own leaf page. PASS = the frame renders real content
|
||||
* (deep element count), and the HOST is untouched: its URL, title and scroll position.
|
||||
* C · script: the partner page pastes the manifest's own embed snippet, with ITS OWN token. PASS = the
|
||||
* element renders, no page error, no failed request to the tree's origin, and every
|
||||
* api.mapbox.com request carries the HOST's token.
|
||||
*
|
||||
* ★ HOW C PROVES THE HOST'S TOKEN IS USED. Handing the partner page the tree's own baked token proves
|
||||
* nothing — a bundle that ignored the host and fell back to its baked key would pass. So the host
|
||||
* passes a DISTINCT, well-formed pk token that exists nowhere in the tree. Every api.mapbox.com
|
||||
* request is intercepted: it must carry that host token (else FAIL: the map used another key), and
|
||||
* is then forwarded with the real token so the map can still paint. A map drawn with zero Mapbox
|
||||
* requests is fine for maps that need none; that is reported, not failed.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { join, dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const argv = process.argv.slice(2)
|
||||
const argOf = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
|
||||
// Default: the seven Research Gallery maps (doxa-maps-spa.meta.json `composes`).
|
||||
const DEFAULT = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.meta.json'), 'utf8')).composes
|
||||
const ONLY = (argOf('--only', '') ? argOf('--only', '').split(',') : DEFAULT).map((s) => s.trim()).filter(Boolean)
|
||||
const CELLS = argOf('--cell', '') ? [argOf('--cell', '').toUpperCase()] : ['B', 'C']
|
||||
|
||||
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
|
||||
if (!existsSync(MANIFEST)) { console.log('MAP-EMBED-CELLS: SKIP — no built tree'); process.exit(2) }
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
|
||||
const bakedToken = (() => {
|
||||
for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
|
||||
if (!p.iframeUrl) continue
|
||||
const leaf = join(ROOT, 'public', 'doxa-maps', String(p.iframeUrl).replace(/^\.\//, ''))
|
||||
if (!existsSync(leaf)) continue
|
||||
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
|
||||
if (m) return m[1]
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
// A well-formed pk token that exists nowhere in the tree (Mapbox GL parses the middle segment).
|
||||
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url')
|
||||
const HOST_TOKEN = 'pk.' + b64({ u: 'partnerhost', a: 'embedprobe' }) + '.hostpartnerprobe'
|
||||
|
||||
const targets = []
|
||||
for (const ref of ONLY) {
|
||||
const [bundle, profile] = ref.split('/')
|
||||
const b = (manifest.bundles || []).find((x) => x.bundle === bundle)
|
||||
const p = b && (b.profiles || []).find((x) => x.profile === profile)
|
||||
if (!b || !p) { targets.push({ ref, missing: true }); continue }
|
||||
const defined = b.elements || []
|
||||
const element = defined.includes(p.element) ? p.element : (defined[0] || p.element)
|
||||
targets.push({ ref, bundle, profile, element, iframeUrl: p.iframeUrl, snippet: (p.embed && p.embed.script) || '' })
|
||||
}
|
||||
|
||||
const freePort = () => new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const port = s.address().port; s.close(() => ok(port)) }) })
|
||||
const TREE_PORT = await freePort()
|
||||
const HOST_PORT = await freePort()
|
||||
const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps`
|
||||
let pageHtml = '<!doctype html><title>host</title><body>nothing</body>'
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')
|
||||
const cleanup = () => { try { tree.kill() } catch (_) {} try { host.close() } catch (_) {} }
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
if (!(await fetch(`${TREE}/manifest.json`, { signal: AbortSignal.timeout(4000) }).then((r) => r.ok).catch(() => false))) {
|
||||
cleanup(); console.log('MAP-EMBED-CELLS: SKIP — could not serve the built tree'); process.exit(2)
|
||||
}
|
||||
const HOST = `http://localhost:${HOST_PORT}/partner.html`
|
||||
|
||||
const deepCount = `(() => { let n = 0, c = 0; const w = (r) => { for (const e of r.querySelectorAll('*')) { n++; if (e.tagName === 'CANVAS') c++; if (e.shadowRoot) w(e.shadowRoot) } }; w(document); return { n, c } })()`
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
|
||||
const rows = []
|
||||
for (const t of targets) {
|
||||
if (t.missing) { rows.push({ ref: t.ref, cell: '-', ok: false, why: 'not in the built manifest' }); continue }
|
||||
for (const cell of CELLS) {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } })
|
||||
const page = await ctx.newPage()
|
||||
const errors = [], treeFails = [], mapboxTokens = new Set()
|
||||
page.on('pageerror', (e) => errors.push(String(e.message || e).slice(0, 140)))
|
||||
page.on('response', (r) => { if (r.status() >= 400 && r.url().startsWith(`http://127.0.0.1:${TREE_PORT}/`)) treeFails.push(r.status() + ' ' + r.url().replace(`http://127.0.0.1:${TREE_PORT}`, '').slice(0, 90)) })
|
||||
await ctx.route(/^https:\/\/api\.mapbox\.com\//, async (route) => {
|
||||
const u = new URL(route.request().url())
|
||||
const tok = u.searchParams.get('access_token')
|
||||
if (tok) { mapboxTokens.add(tok === HOST_TOKEN ? 'HOST' : tok === bakedToken ? 'BAKED' : 'OTHER'); if (tok === HOST_TOKEN) u.searchParams.set('access_token', bakedToken) }
|
||||
await route.continue({ url: u.toString() })
|
||||
})
|
||||
|
||||
let res
|
||||
if (cell === 'B') {
|
||||
const src = `${TREE}/${String(t.iframeUrl).replace(/^\.\//, '')}`
|
||||
pageHtml = `<!doctype html><html><head><meta charset="utf-8"><title>Partner page</title></head><body style="margin:0;font:16px sans-serif">
|
||||
<h1 style="margin:16px">A partner's own page</h1><iframe id="f" src="${src}" width="100%" height="640" style="border:0"></iframe>
|
||||
<div style="height:1400px;padding:16px">Partner content below the map.</div></body></html>`
|
||||
await page.goto(HOST, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForTimeout(14000)
|
||||
const frame = page.frames().find((f) => f.url().startsWith(`http://127.0.0.1:${TREE_PORT}/`))
|
||||
const inside = frame ? await frame.evaluate(deepCount).catch(() => ({ n: 0, c: 0 })) : { n: 0, c: 0 }
|
||||
const hostState = await page.evaluate(() => ({ url: location.href, title: document.title, y: Math.round(scrollY) }))
|
||||
const why = []
|
||||
if (!frame) why.push('iframe never loaded the leaf')
|
||||
if (inside.n < 40) why.push('frame rendered almost nothing (' + inside.n + ' elements)')
|
||||
if (hostState.url !== HOST) why.push('host URL changed to ' + hostState.url)
|
||||
if (hostState.title !== 'Partner page') why.push('host title changed to "' + hostState.title + '"')
|
||||
if (hostState.y !== 0) why.push('host scrolled to y=' + hostState.y)
|
||||
if (treeFails.length) why.push('failed tree requests: ' + [...new Set(treeFails)].slice(0, 3).join(' , '))
|
||||
res = { why, detail: inside.n + ' elements, ' + inside.c + ' canvas in frame; host url/title/scroll unchanged' }
|
||||
} else {
|
||||
if (!t.snippet) { rows.push({ ref: t.ref, cell, ok: false, why: 'manifest has no script embed snippet' }); await ctx.close(); continue }
|
||||
// The manifest's own snippet, made absolute to the tree, with the HOST's token.
|
||||
let snip = t.snippet.replace(/src="\.\//g, `src="${TREE}/`).replace(/YOUR_MAPBOX_TOKEN/g, HOST_TOKEN)
|
||||
snip = snip.replace(/("tk"\s*:\s*")[^"]*(")/g, `$1${HOST_TOKEN}$2`)
|
||||
pageHtml = `<!doctype html><html><head><meta charset="utf-8"><title>Partner page</title></head><body style="margin:0;font:16px sans-serif">
|
||||
<h1 style="margin:16px">A partner's own page</h1><div style="position:relative;height:640px">${snip}</div>
|
||||
<div style="height:600px;padding:16px">Partner content below the map.</div></body></html>`
|
||||
await page.goto(HOST, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForTimeout(14000)
|
||||
const st = await page.evaluate(`(() => { const el = document.querySelector(${JSON.stringify(t.element)}); if (!el) return { present: false }
|
||||
const d = ${deepCount}; return { present: true, defined: !!customElements.get(${JSON.stringify(t.element)}), n: d.n, c: d.c, h: Math.round(el.getBoundingClientRect().height) } })()`)
|
||||
const why = []
|
||||
if (!st.present) why.push('snippet did not put <' + t.element + '> on the page')
|
||||
else {
|
||||
if (!st.defined) why.push('<' + t.element + '> never registered')
|
||||
if (st.n < 40) why.push('rendered almost nothing (' + st.n + ' elements)')
|
||||
if (st.h < 100) why.push('element is ' + st.h + 'px tall')
|
||||
}
|
||||
if (mapboxTokens.has('BAKED') || mapboxTokens.has('OTHER')) why.push('Mapbox requests used a token other than the HOST\'s: ' + [...mapboxTokens].join(', '))
|
||||
if (treeFails.length) why.push('failed tree requests: ' + [...new Set(treeFails)].slice(0, 3).join(' , '))
|
||||
if (errors.length) why.push('page error: ' + [...new Set(errors)].slice(0, 2).join(' / '))
|
||||
res = { why, detail: st.n + ' elements, ' + st.c + ' canvas, ' + st.h + 'px; Mapbox token(s): ' + ([...mapboxTokens].join(',') || 'none requested') }
|
||||
}
|
||||
rows.push({ ref: t.ref, cell, ok: !res.why.length, why: res.why.join(' | '), detail: res.detail })
|
||||
await ctx.close()
|
||||
}
|
||||
}
|
||||
await browser.close(); cleanup()
|
||||
|
||||
for (const r of rows) console.log((r.ok ? 'PASS ' : 'FAIL ') + r.cell + ' ' + r.ref.padEnd(46) + (r.ok ? r.detail : r.why))
|
||||
const bad = rows.filter((r) => !r.ok)
|
||||
console.log(bad.length ? `MAP-EMBED-CELLS: FAIL — ${bad.length} of ${rows.length} cell(s)` : `MAP-EMBED-CELLS: PASS — ${rows.length} cell(s) on a foreign origin`)
|
||||
process.exit(bad.length ? 1 : 0)
|
||||
|
|
@ -203,6 +203,8 @@ async function measure(width, lang) {
|
|||
const boxes = cards.map((c) => ({ c, b: c.getBoundingClientRect() }))
|
||||
let intrusions = 0, overflowing = 0, worst = 0, coveringPreview = 0, worstY = 0
|
||||
let previewsPainted = 0, previewsBroken = 0, noPreview = 0, unnamedPreview = 0
|
||||
let notTaped = 0, worstTape = 0, selectsInRow = 0
|
||||
let headNotTaped = 0, worstHead = 0, mapCovered = 0, worstCover = 0
|
||||
for (const { c, b } of boxes) {
|
||||
const row = c.querySelector('.pfs')
|
||||
if (!row) continue
|
||||
|
|
@ -247,6 +249,69 @@ async function measure(width, lang) {
|
|||
worstY = Math.max(worstY, Math.round(ib.bottom - rb.top))
|
||||
}
|
||||
}
|
||||
// ★ A STICKER STRADDLES THE EDGE IT TAPES (Driver 2026-09-25, the sticker/banner brief).
|
||||
// The card is a BANNER displaying a map; each sticker sits centred on a banner edge, part
|
||||
// on the banner's own white space and part overhanging. Three things were each true while
|
||||
// this gate was green, so all three are now asserted:
|
||||
// 1. the row floated 26px BELOW the banner, touching nothing (the shipped state);
|
||||
// 2. the row sat entirely below the edge, touching at one pixel;
|
||||
// 3. the row straddled correctly but landed ON THE MAP — "offensive to the map".
|
||||
// over-preview above catches only a row overlapping the <img> from above.
|
||||
const frame = c.querySelector('.main')
|
||||
if (frame) {
|
||||
const fb = frame.getBoundingClientRect()
|
||||
// STRADDLE: some of the sticker on the banner, some off it. Requiring a near-even split
|
||||
// would encode a ratio the Driver explicitly left open ("probably an equal or more
|
||||
// overlap"); requiring BOTH sides to be non-trivial is the property actually stated.
|
||||
const onBanner = Math.round(fb.bottom - rb.top)
|
||||
const offBanner = Math.round(rb.bottom - fb.bottom)
|
||||
if (onBanner < 4 || offBanner < 4) {
|
||||
notTaped++
|
||||
const d = onBanner < 4 ? onBanner : -offBanner
|
||||
worstTape = Math.abs(d) > Math.abs(worstTape) ? d : worstTape
|
||||
}
|
||||
}
|
||||
// ★ NO NATIVE DROPDOWN IN THE DEFAULT ROW (same ruling). The Driver's reason is not
|
||||
// cosmetic: "I don't want to add the dropdown for the card because it makes it hard for
|
||||
// others to have dropdowns too" — a <select> here collides with the host page's own
|
||||
// selects. The size picker is a disclosure, revealed by the Card Deck button. A <select>
|
||||
// reappearing in .pfs is the regression this catches.
|
||||
if (row.querySelector('select')) selectsInRow++
|
||||
// ★ AND THE HEADER STICKER STRADDLES ITS EDGE TOO. Same requirement, other end of the
|
||||
// banner, and it was ALSO wrong while everything here was green: MEASURED 35px above the
|
||||
// banner's top edge and -1px below, i.e. floating over the card touching the banner
|
||||
// nowhere. Asserting only the footer would have left that in place indefinitely.
|
||||
const headSticker = c.querySelector('.tape-h')
|
||||
if (frame && headSticker) {
|
||||
const hb = headSticker.getBoundingClientRect()
|
||||
const fb2 = frame.getBoundingClientRect()
|
||||
const onB = Math.round(hb.bottom - fb2.top)
|
||||
const offB = Math.round(fb2.top - hb.top)
|
||||
if (onB < 4 || offB < 4) {
|
||||
headNotTaped++
|
||||
const d = onB < 4 ? onB : -offB
|
||||
worstHead = Math.abs(d) > Math.abs(worstHead) ? d : worstHead
|
||||
}
|
||||
}
|
||||
// ★ NOTHING TOUCHES THE MAP. The brief is unambiguous and is the reason the banner grew
|
||||
// at all: "the preview banner to showcase a map should not have anything overlapping the
|
||||
// map because the map needs to be fully visible." Measured on the first straddle attempt:
|
||||
// header over the map by 6px, footer by 8px — a correct straddle onto a banner with too
|
||||
// little white space reserved. THIS is the assertion that catches that, and neither the
|
||||
// straddle checks above nor over-preview can: over-preview only sees a row whose top is
|
||||
// above the image bottom, which is a different geometry from a sticker landing on it.
|
||||
if (img) {
|
||||
const ib3 = img.getBoundingClientRect()
|
||||
if (ib3.height > 2) {
|
||||
const byFoot = Math.round(ib3.bottom - rb.top)
|
||||
if (byFoot > 0) { mapCovered++; worstCover = Math.max(worstCover, byFoot) }
|
||||
if (headSticker) {
|
||||
const hb2 = headSticker.getBoundingClientRect()
|
||||
const byHead = Math.round(hb2.bottom - ib3.top)
|
||||
if (byHead > 0) { mapCovered++; worstCover = Math.max(worstCover, byHead) }
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const o of boxes) {
|
||||
if (o.c === c) continue
|
||||
const ox = Math.min(rb.right, o.b.right) - Math.max(rb.left, o.b.left)
|
||||
|
|
@ -295,7 +360,8 @@ async function measure(width, lang) {
|
|||
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
|
||||
overflowing, worst, intrusions, coveringPreview, worstY,
|
||||
previewsPainted, previewsBroken, noPreview, unnamedPreview, clipped: clipped.length,
|
||||
controls: ctrls.length, tiny: tiny.length, perRow, cardW }
|
||||
controls: ctrls.length, tiny: tiny.length, perRow, cardW,
|
||||
notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover }
|
||||
})()`)
|
||||
await page.close()
|
||||
return { width, lang, hover, reach, ...out }
|
||||
|
|
@ -315,6 +381,10 @@ for (const r of results) {
|
|||
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
|
||||
+ ` · neighbour ${r.intrusions}`
|
||||
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
|
||||
+ ` · off-frame ${r.notTaped}${r.worstTape ? ` (${r.worstTape > 0 ? '+' : ''}${r.worstTape}px)` : ''}`
|
||||
+ ` · selects ${r.selectsInRow}`
|
||||
+ ` · head-off ${r.headNotTaped}${r.worstHead ? ` (${r.worstHead > 0 ? '+' : ''}${r.worstHead}px)` : ''}`
|
||||
+ ` · map-covered ${r.mapCovered}${r.worstCover ? ` (${r.worstCover}px)` : ''}`
|
||||
+ ` · previews ${r.previewsPainted} painted`
|
||||
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
|
||||
+ ` · max ${r.perRow}/row @ ${r.cardW}px`
|
||||
|
|
@ -362,6 +432,39 @@ for (const r of results) {
|
|||
fails.push(`${at}: ${r.previewsBroken} preview image(s) failed to load (naturalWidth 0) — `
|
||||
+ 'a broken preview is not something the geometry checks can see')
|
||||
}
|
||||
// ★ THE OTHER EDGE OF over-preview. That assertion catches a row sitting too HIGH; this one
|
||||
// catches it sitting too LOW, which is the state that actually shipped and that nothing here
|
||||
// could see: MEASURED frame bottom 426, row top 452, and a full green run.
|
||||
if (r.notTaped) {
|
||||
fails.push(`${at}: ${r.notTaped} print row(s) do not STRADDLE the banner's bottom edge `
|
||||
+ `(worst ${r.worstTape > 0 ? '+' : ''}${r.worstTape}px on one side; positive = too little `
|
||||
+ 'on the banner, negative = too little overhanging). The footer sticker sits centred on '
|
||||
+ 'that edge, part on the banner\'s white space and part hanging onto the page below — '
|
||||
+ '"slight overlap over the banner, but not over the content of the banner". A row '
|
||||
+ 'floating clear of the banner is as wrong as one covering the map.')
|
||||
}
|
||||
// ★ THE ONE THE DRIVER CARES ABOUT MOST. "The preview banner to showcase a map should not
|
||||
// have anything overlapping the map because the map needs to be fully visible."
|
||||
if (r.mapCovered) {
|
||||
fails.push(`${at}: ${r.mapCovered} sticker(s) OVERLAP the map preview (worst ${r.worstCover}px). `
|
||||
+ 'The banner\'s header and footer stickers straddle the banner\'s edges; the map inside is '
|
||||
+ 'never to be touched — "if I put a piece of tape over the text it would be offensive to '
|
||||
+ 'the map". Reserve more banner white space via --banner-pad-top/--banner-pad-bottom on '
|
||||
+ '.main rather than moving the sticker: a sticker centred on the edge puts HALF its '
|
||||
+ 'height on the banner, so the padding must exceed that half.')
|
||||
}
|
||||
if (r.headNotTaped) {
|
||||
fails.push(`${at}: ${r.headNotTaped} header sticker(s) do not straddle the banner's top edge `
|
||||
+ `(worst ${r.worstHead > 0 ? '+' : ''}${r.worstHead}px on one side). A sticker sits ON the `
|
||||
+ 'edge it tapes, part on the banner and part overhanging — measured 35px above / -1px '
|
||||
+ 'below before this was asserted, i.e. floating free of the banner entirely.')
|
||||
}
|
||||
if (r.selectsInRow) {
|
||||
fails.push(`${at}: ${r.selectsInRow} native <select>(s) in the print row. The card-stock `
|
||||
+ 'picker is a DISCLOSURE behind the Card Deck button, not a control in the default row — '
|
||||
+ '"I don\'t want to add the dropdown for the card because it makes it hard for others to '
|
||||
+ 'have dropdowns too." A <select> here also collides with the host page\'s own dropdowns.')
|
||||
}
|
||||
// EVERY card falling back to the placeholder means the previews are gone. MEASURED by aborting
|
||||
// every previews/** request: the product degrades WELL — all 27 cards swap to a .ph placeholder
|
||||
// reading "poster not generated yet", with no broken-image icons and no <img> left at all. That
|
||||
|
|
|
|||
66
internal/check-vision-card-numbers.mjs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-vision-card-numbers — the DOXA Vision Card shows the numbers the DATA gives, nothing typed.
|
||||
*
|
||||
* USAGE: node internal/check-vision-card-numbers.mjs (data only, no browser, ~2 s)
|
||||
* node internal/check-vision-card-numbers.mjs --url <card> (also read the RENDERED card)
|
||||
* Exit 0 = every number matches · 1 = a mismatch.
|
||||
*
|
||||
* INDEPENDENT RECOMPUTATION. It does not import build-vision-funnel.mjs: it re-reads the raw CSV with
|
||||
* its own column lookup and re-applies the published rules (HOW-THE-NUMBERS-ARE-COUNTED.md) to the
|
||||
* saved DOXA list, then compares with (1) data/people-groups/vision-funnel.json, which the card
|
||||
* compiles in, and (2) with --url, the numbers actually painted in the card's bars. A typed-in
|
||||
* number, a stale funnel file or a rendering bug each makes it fail.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const text = readFileSync(resolve(ROOT, 'data/people-groups/people_groups.csv'), 'utf8')
|
||||
|
||||
// Own CSV reader: split records on newlines outside quotes, fields on commas outside quotes.
|
||||
const records = []; { let cur = ''; let q = false; for (const ch of text) { if (ch === '"') q = !q; if (ch === '\n' && !q) { records.push(cur); cur = '' } else cur += ch } if (cur.trim()) records.push(cur) }
|
||||
const split = (line) => { const out = []; let f = ''; let q = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') { if (q && line[i + 1] === '"') { f += '"'; i++ } else q = !q } else if (c === ',' && !q) { out.push(f); f = '' } else if (c !== '\r') f += c } out.push(f); return out }
|
||||
const head = split(records[0]).map((h) => h.replace(/^/, ''))
|
||||
const col = (n) => head.indexOf(n)
|
||||
const L = col('LostnessPriority'), R3 = col('ROP3Code'), ISO = col('ISOAlpha3')
|
||||
const data = records.slice(1).map(split).filter((f) => f.length === head.length)
|
||||
const expect = {
|
||||
all: data.length,
|
||||
unreached: data.filter((f) => ['0', '1', '2'].includes(f[L])).length,
|
||||
frontier: data.filter((f) => f[L] === '0').length,
|
||||
}
|
||||
const snap = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/doxa-groups-snapshot.json'), 'utf8'))
|
||||
const lost = new Map(data.map((f) => [f[R3] + '|' + f[ISO], f[L]]))
|
||||
expect.doxa = snap.groups.length
|
||||
expect.doxaFrontier = snap.groups.filter((g) => lost.get(g.rop3 + '|' + g.country) === '0').length
|
||||
|
||||
const funnel = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/vision-funnel.json'), 'utf8'))
|
||||
const got = Object.fromEntries(funnel.steps.map((s) => [s.id, s.groups]))
|
||||
got.doxaFrontier = funnel.steps.find((s) => s.id === 'doxa').frontier
|
||||
const bad = []
|
||||
for (const k of Object.keys(expect)) {
|
||||
const ok = expect[k] === got[k]
|
||||
console.log((ok ? 'ok ' : 'FAIL ') + k.padEnd(13) + ' recomputed ' + String(expect[k]).padStart(6) + ' vision-funnel.json ' + got[k])
|
||||
if (!ok) bad.push(k)
|
||||
}
|
||||
|
||||
const i = process.argv.indexOf('--url')
|
||||
if (i > -1) {
|
||||
const { chromium } = await import('playwright')
|
||||
const b = await chromium.launch()
|
||||
const p = await b.newPage({ viewport: { width: 900, height: 1000 } })
|
||||
await p.goto(process.argv[i + 1], { waitUntil: 'networkidle' })
|
||||
await p.waitForTimeout(1500)
|
||||
const shown = await p.evaluate(() => { const el = document.querySelector('doxa-vision-card'); const r = el && el.shadowRoot; return r ? [...r.querySelectorAll('.vc-n')].map((n) => n.textContent.trim()) : [] })
|
||||
const note = await p.evaluate(() => { const r = document.querySelector('doxa-vision-card')?.shadowRoot; return r ? r.querySelector('.vc-step-doxa .vc-note')?.textContent || '' : '' })
|
||||
await b.close()
|
||||
const want = [expect.all, expect.unreached, expect.frontier, expect.doxa].map((n) => new Intl.NumberFormat('en').format(n))
|
||||
const okBars = JSON.stringify(shown) === JSON.stringify(want)
|
||||
console.log((okBars ? 'ok ' : 'FAIL ') + 'rendered bars ' + JSON.stringify(shown) + (okBars ? '' : ' expected ' + JSON.stringify(want)))
|
||||
const okNote = note.includes(new Intl.NumberFormat('en').format(expect.doxaFrontier))
|
||||
console.log((okNote ? 'ok ' : 'FAIL ') + 'rendered DOXA note "' + note.trim() + '"')
|
||||
if (!okBars) bad.push('rendered bars'); if (!okNote) bad.push('rendered note')
|
||||
}
|
||||
console.log(bad.length ? 'VISION-CARD-NUMBERS: FAIL — ' + bad.join(', ') : 'VISION-CARD-NUMBERS: PASS — every number matches the data')
|
||||
process.exit(bad.length ? 1 : 0)
|
||||
|
|
@ -1438,6 +1438,8 @@ function main() {
|
|||
// 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',
|
||||
// …and its compact cut (build-points.mjs): what all-people-groups actually fetches, 0.74 MB.
|
||||
'people-groups/people-groups-points.json',
|
||||
// vendor — DataTable/TabulatorTable inject Tabulator from assetsBase at runtime.
|
||||
'vendor/tabulator/tabulator.min.js',
|
||||
'vendor/tabulator/tabulator.min.css',
|
||||
|
|
|
|||
|
|
@ -76,15 +76,35 @@ const CONCURRENCY = Math.max(
|
|||
) || 1
|
||||
)
|
||||
|
||||
// The rendezvous every agent's browser gate queues on. It must be ONE path for all of them or
|
||||
// the queue is not a queue, so it is an ENV VAR with a product-named default: the shared file
|
||||
// several agents' shells already export lives outside this repo and carries an internal code
|
||||
// name, which check:no-codename correctly refuses to let a handed-over repo carry. Export
|
||||
// BROWSER_GATE_LOCK to join whatever rendezvous the rest of the box is using.
|
||||
const BROWSER_LOCK = process.env.BROWSER_GATE_LOCK || '/tmp/doxa-maps-browser-gate.lock'
|
||||
const LOAD_MAX = Number(process.env.LOAD_MAX || 12)
|
||||
const QUIET_WAIT_MAX = Number(process.env.QUIET_WAIT_MAX || 900)
|
||||
// ★ TWO LOCKS, IN A FIXED ORDER, AND THAT IS NOT BELT-AND-BRACES — IT IS A NAME PROBLEM.
|
||||
// The rendezvous must be ONE path for everyone or the queue is not a queue. The agents' own probe
|
||||
// scripts take a lock whose filename carries an internal code name, and check:no-codename refuses
|
||||
// (correctly) to let a repo we are handing over contain that word. So this takes BOTH: the repo's
|
||||
// product-named lock, and whatever path the environment names — always in that order, so two
|
||||
// processes can never take them in opposite orders and deadlock. Either file alone still
|
||||
// serialises everything that uses it; taking both is what makes gates and hand-run probes queue
|
||||
// against each other without the repo having to spell a name it must not ship.
|
||||
const BROWSER_LOCK = '/tmp/doxa-maps-browser-gate.lock'
|
||||
const EXTRA_LOCK = process.env.BROWSER_GATE_LOCK && process.env.BROWSER_GATE_LOCK !== BROWSER_LOCK
|
||||
? process.env.BROWSER_GATE_LOCK
|
||||
: ''
|
||||
// ★ THE LOAD WAIT WAS COSTING MORE THAN IT SAVED. It was written at LOAD_MAX 12 when the risk was
|
||||
// seven agents starting chrome at once; the FLOCK below is what actually prevents that — one
|
||||
// browser gate at a time, across every agent on the box. The load check is belt-and-braces on top
|
||||
// of a belt. MEASURED 2026-09-25 with several agents working: 1-min load sits at 36-50 all day, so
|
||||
// 12 is unreachable and every browser gate simply waited out its full 900s cap before running
|
||||
// anyway. That is a 15-minute tax per wave for a guard that never opens, and its practical effect
|
||||
// is that people stop running browser gates. Raised to a level that still catches a genuinely
|
||||
// wedged box, with a short cap so a wave is never held for long. Both still override by env.
|
||||
// Scaled to the machine rather than a constant: a load of 40 means something different on 8 cores
|
||||
// than on 64. Five runnable processes per core is busy-but-working; past that the box is thrashing
|
||||
// and a browser gate would be measuring the thrash.
|
||||
const LOAD_MAX = Number(process.env.LOAD_MAX || cpus().length * 5)
|
||||
const QUIET_WAIT_MAX = Number(process.env.QUIET_WAIT_MAX || 120)
|
||||
const HAS_FLOCK = spawnSync('sh', ['-c', 'command -v flock'], { stdio: 'ignore' }).status === 0
|
||||
const LOCK_WAIT_S = Number(process.env.BROWSER_LOCK_WAIT || 900)
|
||||
// EX_TEMPFAIL. Anything but 0 or 1, so it cannot be confused with a gate's own verdict.
|
||||
const LOCK_BUSY_CODE = 75
|
||||
|
||||
/**
|
||||
* Does this step drive a real browser? Asked of the GATE FILE, not of a hand-kept list:
|
||||
|
|
@ -157,7 +177,18 @@ function buildSteps() {
|
|||
// wait-for-quiet.sh takes the same lock), so this queues rather than dogpiles. -w 1200
|
||||
// caps the wait so a crashed holder cannot wedge a wave forever, and static gates —
|
||||
// parse, sri, hygiene, the twenty that are node-startup — are left completely free.
|
||||
return { ...step, browser: true, cmd: 'flock', args: ['-w', '1200', BROWSER_LOCK, 'bun', 'run', name] }
|
||||
// flock can exec flock: nesting is how one command waits on both rendezvous points.
|
||||
// ★ -E 75: A LOCK TIMEOUT IS NOT A GATE FAILURE, AND flock CANNOT SAY SO BY DEFAULT.
|
||||
// Without it flock exits 1 on timeout — the same code a failing gate uses — and a wave that
|
||||
// simply could not get the browser lock prints ✗ against five product gates. MEASURED: five
|
||||
// browser gates each "failed" at exactly 1200064ms, which is the timeout to the millisecond
|
||||
// and not a verdict about the product at all. 75 (EX_TEMPFAIL) is distinct, so the runner can
|
||||
// report "could not take the lock" as the precondition it is.
|
||||
const lockArgs = ['-w', String(LOCK_WAIT_S), '-E', String(LOCK_BUSY_CODE)]
|
||||
const args = EXTRA_LOCK
|
||||
? [...lockArgs, BROWSER_LOCK, 'flock', ...lockArgs, EXTRA_LOCK, 'bun', 'run', name]
|
||||
: [...lockArgs, BROWSER_LOCK, 'bun', 'run', name]
|
||||
return { ...step, browser: true, cmd: 'flock', args }
|
||||
})
|
||||
}
|
||||
// A bundle is a plugin-designs/<name>/ directory — the same selector build:unlocked used
|
||||
|
|
@ -217,9 +248,12 @@ async function main() {
|
|||
// check:packets-all SKIPped on every CI build for exactly this reason and nobody
|
||||
// could see it, because the wave reported it green. An always-taken SKIP is
|
||||
// indistinguishable from an unwired gate unless the runner says so.
|
||||
const skipped = /(^|\n)\s*[A-Z][A-Z0-9-]*:\s*SKIP\b/.test(r.out)
|
||||
// A gate that never ran because the browser lock stayed busy is UNMEASURED, not failed.
|
||||
const lockBusy = r.browser && r.code === LOCK_BUSY_CODE
|
||||
const skipped = lockBusy || /(^|\n)\s*[A-Z][A-Z0-9-]*:\s*SKIP\b/.test(r.out)
|
||||
if (lockBusy) r.out = (r.out || '') + `\n (could not take the browser lock within ${LOCK_WAIT_S}s — not a product finding)`
|
||||
console.log(
|
||||
` ${r.code !== 0 ? '✗' : skipped ? '⊘' : '✓'} ${r.name.padEnd(26)} ${String(r.ms).padStart(7)}ms` +
|
||||
` ${skipped ? '⊘' : r.code !== 0 ? '✗' : '✓'} ${r.name.padEnd(26)} ${String(r.ms).padStart(7)}ms` +
|
||||
(r.code === 0 ? '' : ` exit ${r.code}`)
|
||||
)
|
||||
r.skipped = skipped
|
||||
|
|
@ -229,7 +263,7 @@ async function main() {
|
|||
|
||||
const wall = Date.now() - t0
|
||||
const serial = results.reduce((a, r) => a + r.ms, 0)
|
||||
const failed = results.filter((r) => r.code !== 0)
|
||||
const failed = results.filter((r) => r.code !== 0 && !(r.browser && r.code === LOCK_BUSY_CODE))
|
||||
|
||||
if (VERBOSE) for (const r of results) if (r.out.trim()) console.log(`\n───── ${r.name} ─────\n${r.out}`)
|
||||
// A failure must be readable in full: these gates explain themselves in their output, and
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
* useMapData when normalizing). If facets aren't ready yet, we render a
|
||||
* placeholder so the panel never blocks initial paint.
|
||||
*/
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
// See ResearchMapSideMenu for why this reads the catalogue directly rather than through useI18n.
|
||||
const L = (key, en) => tStatic('controls.' + key) || en
|
||||
|
||||
import { inject, computed, watch, ref } from 'vue'
|
||||
import { useShadowStyles } from '../composables/useShadowStyles.js'
|
||||
|
|
@ -146,37 +149,37 @@ useShadowStyles(`
|
|||
<div class="rm-filter">
|
||||
<!-- Population range ──────────────────────────────────────────────── -->
|
||||
<div class="field">
|
||||
<label>Population</label>
|
||||
<label>{{ L('population', 'Population') }}</label>
|
||||
<div class="rm-pop-row">
|
||||
<input type="number" min="0" placeholder="min" :value="popMin" @input="popMin = $event.target.value" />
|
||||
<input type="number" min="0" placeholder="max" :value="popMax" @input="popMax = $event.target.value" />
|
||||
<input type="number" min="0" :placeholder="L('min', 'min')" :value="popMin" @input="popMin = $event.target.value" />
|
||||
<input type="number" min="0" :placeholder="L('max', 'max')" :value="popMax" @input="popMax = $event.target.value" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Engagement / adoption status ──────────────────────────────────── -->
|
||||
<div class="field">
|
||||
<label>Engagement</label>
|
||||
<label>{{ L('engagement', 'Engagement') }}</label>
|
||||
<select :value="engagement" @change="engagement = $event.target.value">
|
||||
<option value="any">Any</option>
|
||||
<option value="engaged">Engaged</option>
|
||||
<option value="unengaged">Unengaged</option>
|
||||
<option value="engaged">{{ L('engaged', 'Engaged') }}</option>
|
||||
<option value="unengaged">{{ L('unengaged', 'Unengaged') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Adoption</label>
|
||||
<label>{{ L('adoption', 'Adoption') }}</label>
|
||||
<select :value="adoption" @change="adoption = $event.target.value">
|
||||
<option value="any">Any</option>
|
||||
<option value="adopted">Adopted</option>
|
||||
<option value="unadopted">Unadopted</option>
|
||||
<option value="adopted">{{ L('adopted', 'Adopted') }}</option>
|
||||
<option value="unadopted">{{ L('unadopted', 'Unadopted') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Language multi-select ─────────────────────────────────────────── -->
|
||||
<div class="field">
|
||||
<label>Language</label>
|
||||
<input type="search" v-model="langSearch" placeholder="Search languages…" />
|
||||
<label>{{ L('language', 'Language') }}</label>
|
||||
<input type="search" v-model="langSearch" :placeholder="L('search_languages', 'Search languages…')" />
|
||||
<div class="rm-chips">
|
||||
<span v-if="!filteredLangs.length" class="rm-empty">No languages loaded yet</span>
|
||||
<span v-if="!filteredLangs.length" class="rm-empty">{{ L('no_languages', 'No languages loaded yet') }}</span>
|
||||
<button
|
||||
v-for="lang in filteredLangs"
|
||||
:key="lang"
|
||||
|
|
@ -189,10 +192,10 @@ useShadowStyles(`
|
|||
|
||||
<!-- Religion multi-select ─────────────────────────────────────────── -->
|
||||
<div class="field">
|
||||
<label>Religion</label>
|
||||
<input type="search" v-model="relSearch" placeholder="Search religions…" />
|
||||
<label>{{ L('religion', 'Religion') }}</label>
|
||||
<input type="search" v-model="relSearch" :placeholder="L('search_religions', 'Search religions…')" />
|
||||
<div class="rm-chips">
|
||||
<span v-if="!filteredRels.length" class="rm-empty">No religions loaded yet</span>
|
||||
<span v-if="!filteredRels.length" class="rm-empty">{{ L('no_religions', 'No religions loaded yet') }}</span>
|
||||
<button
|
||||
v-for="rel in filteredRels"
|
||||
:key="rel"
|
||||
|
|
@ -206,7 +209,7 @@ useShadowStyles(`
|
|||
<!-- Footer: active count + reset link ─────────────────────────────── -->
|
||||
<div class="rm-filter-footer">
|
||||
<span class="rm-pill">{{ activeCount }} active</span>
|
||||
<button class="rm-link-btn" @click="resetAll">Reset all</button>
|
||||
<button class="rm-link-btn" @click="resetAll">{{ L('reset_all', 'Reset all') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -32,15 +32,24 @@ defineProps({
|
|||
})
|
||||
const emit = defineEmits(['close', 'open-poster', 'reload-map'])
|
||||
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
// ★ THIS PANEL WAS ENGLISH IN ALL ELEVEN LOCALES. Measured on the composite under ?lang=ru: every
|
||||
// Latin-script string left on the page belonged to this menu and its filter sibling — the map
|
||||
// around them was fully translated. Read through tStatic rather than useI18n: these components are
|
||||
// reachable from mounts that install no i18n plugin, and an unguarded useI18n() throws and takes
|
||||
// the subtree with it (the defect that blanked people-groups-grid, fixed in 6603dc17). English
|
||||
// stays as the last argument, so a missing key degrades to today's text rather than to a key name.
|
||||
const L = (key, en) => tStatic('controls.' + key) || en
|
||||
|
||||
const mapStore = inject('mapStore')
|
||||
const uiStore = inject('uiStore')
|
||||
|
||||
// ─── Cluster controls ────────────────────────────────────────────────────────
|
||||
const CLUSTER_MODES = [
|
||||
{ id: 'off', label: 'Off', desc: 'Show every pin individually' },
|
||||
{ id: 'mapbox', label: 'Native', desc: 'Mapbox circle clusters (fastest)' },
|
||||
{ id: 'mst', label: 'MST', desc: 'Minimum-spanning-tree affinity webs' },
|
||||
{ id: 'network', label: 'Network', desc: 'Nearest-neighbor connection lines' }
|
||||
{ id: 'off', label: L('off', 'Off'), desc: L('off_desc', 'Show every pin individually') },
|
||||
{ id: 'mapbox', label: L('native', 'Native'), desc: L('native_desc', 'Mapbox circle clusters (fastest)') },
|
||||
{ id: 'mst', label: L('mst', 'MST'), desc: L('mst_desc', 'Minimum-spanning-tree affinity webs') },
|
||||
{ id: 'network', label: L('network', 'Network'), desc: L('network_desc', 'Nearest-neighbor connection lines') }
|
||||
]
|
||||
const clusterMode = computed({
|
||||
get: () => mapStore?.clusteringMode ?? 'off',
|
||||
|
|
@ -87,14 +96,14 @@ useShadowStyles(`
|
|||
<div class="rm-side-backdrop" :class="{ open: isOpen }" @click="emit('close')" />
|
||||
<aside class="rm-side-drawer" :class="{ open: isOpen }" role="dialog" aria-label="Research map controls">
|
||||
<header class="rm-side-header">
|
||||
<h3 class="rm-side-title">Research controls</h3>
|
||||
<h3 class="rm-side-title">{{ L('research_controls', 'Research controls') }}</h3>
|
||||
<button class="rm-side-close" aria-label="Close" @click="emit('close')">×</button>
|
||||
</header>
|
||||
|
||||
<div class="rm-side-body">
|
||||
<!-- Clustering ─────────────────────────────────────────────────── -->
|
||||
<div class="rm-section">
|
||||
<h4>Clustering</h4>
|
||||
<h4>{{ L('clustering', 'Clustering') }}</h4>
|
||||
<div class="rm-cluster-grid">
|
||||
<button
|
||||
v-for="m in CLUSTER_MODES"
|
||||
|
|
@ -108,7 +117,7 @@ useShadowStyles(`
|
|||
</button>
|
||||
</div>
|
||||
<div v-if="clusterMode !== 'off'" class="rm-slider-row">
|
||||
<label for="rm-pin-op">Pin dim</label>
|
||||
<label for="rm-pin-op">{{ L('pin_dim', 'Pin dim') }}</label>
|
||||
<input
|
||||
id="rm-pin-op"
|
||||
type="range"
|
||||
|
|
@ -124,14 +133,14 @@ useShadowStyles(`
|
|||
|
||||
<!-- Filter panel slot ──────────────────────────────────────────── -->
|
||||
<div class="rm-section">
|
||||
<h4>Filters</h4>
|
||||
<h4>{{ L('filters', 'Filters') }}</h4>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="rm-side-footer">
|
||||
<button class="rm-btn" @click="emit('open-poster')">Export poster…</button>
|
||||
<button class="rm-btn ghost" @click="emit('reload-map')">Reload data</button>
|
||||
<button class="rm-btn" @click="emit('open-poster')">{{ L('export_poster', 'Export poster…') }}</button>
|
||||
<button class="rm-btn ghost" @click="emit('reload-map')">{{ L('reload_data', 'Reload data') }}</button>
|
||||
</footer>
|
||||
</aside>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -187,9 +187,28 @@ function storePerRow(n) {
|
|||
}
|
||||
// grid-template-columns, or nothing at all when the count is 0 — an empty string leaves the
|
||||
// stylesheet's auto-fill rule in charge rather than overriding it with something equivalent.
|
||||
const gridStyle = computed(() => perRow.value > 0
|
||||
? { gridTemplateColumns: `repeat(${perRow.value}, minmax(0, 1fr))` }
|
||||
: {})
|
||||
// ⚠ THE PREFERENCE IS CLAMPED BY WHAT THE VIEWPORT CAN ACTUALLY HOLD.
|
||||
// An inline style beats the stylesheet, so `repeat(2, minmax(0,1fr))` overrode the CSS rule's
|
||||
// `minmax(210px,1fr)` at EVERY width — including 390px, where two columns give a ~162px card.
|
||||
// Reported from a 390x844 phone shot: two posters per row, where the CSS alone would have
|
||||
// given one. The stored preference is the Driver's and must survive; it is a preference for
|
||||
// DESKTOP density, not an instruction to overflow a phone.
|
||||
// 210px is the same minimum the stylesheet's auto-fill uses, so the clamp agrees with the rule
|
||||
// it is overriding rather than inventing a second breakpoint.
|
||||
const CARD_MIN_PX = 210
|
||||
const viewportW = ref(typeof window !== 'undefined' ? window.innerWidth : 1280)
|
||||
if (typeof window !== 'undefined') {
|
||||
const onResize = () => { viewportW.value = window.innerWidth }
|
||||
window.addEventListener('resize', onResize)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||
}
|
||||
const gridStyle = computed(() => {
|
||||
if (!(perRow.value > 0)) return {}
|
||||
// The wall's own padding either side; err toward FEWER columns rather than a clipped card.
|
||||
const usable = Math.max(0, viewportW.value - 36)
|
||||
const fits = Math.max(1, Math.floor(usable / CARD_MIN_PX))
|
||||
return { gridTemplateColumns: `repeat(${Math.min(perRow.value, fits)}, minmax(0, 1fr))` }
|
||||
})
|
||||
|
||||
const plural = computed(() => props.itemNounPlural || `${props.itemNoun}s`)
|
||||
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
|
||||
|
|
@ -409,6 +428,31 @@ const prints = usePosterPrints({
|
|||
const canPrintRows = computed(() => typeof props.records === 'function')
|
||||
const previewIsControl = computed(() => props.previewAction === 'print')
|
||||
|
||||
// ── CARD-DECK DISCLOSURE ────────────────────────────────────────────────────
|
||||
// Which card currently has its size row revealed, by item id. Holding an ID
|
||||
// rather than a boolean-per-card means opening one CLOSES the others for free:
|
||||
// two size rows open at once would overlap in the grid gap, and there is no
|
||||
// reading of the Driver's request where two are wanted.
|
||||
const deckOpenFor = ref(null)
|
||||
function toggleDeck(id, ev) {
|
||||
deckOpenFor.value = deckOpenFor.value === id ? null : id
|
||||
// Tell the size row how far down to start: the print row's REAL height, which is
|
||||
// one line in en and two in the long-label locales. Walking up from the clicked
|
||||
// button to its own .pfs keeps this correct per card rather than per wall.
|
||||
const pfs = ev?.currentTarget?.closest?.('.pfs')
|
||||
if (pfs) pfs.parentElement?.style.setProperty('--pstocks-gap', `${pfs.offsetHeight}px`)
|
||||
}
|
||||
|
||||
// Choosing a size IS the print command — the reader already said "print the
|
||||
// cards" by opening this row, so a second confirm step would be a click with
|
||||
// nothing behind it. prints.stock reads pickedStock through a getter, so setting
|
||||
// it here is what the deck prints at.
|
||||
function printDeckAt(item, stockId) {
|
||||
pickedStock.value = stockId
|
||||
deckOpenFor.value = null
|
||||
prints.printCardDeck(item)
|
||||
}
|
||||
|
||||
function downloadPacket(item) {
|
||||
// Still the honest "not yet" when this wall has no records provider: NOT an
|
||||
// error — unbuilt, not broken. phase:'error' reads to a user as "something went
|
||||
|
|
@ -479,12 +523,21 @@ function download(blob, name) {
|
|||
function dismissExport() { exportState.value = null }
|
||||
|
||||
useShadowStyles(
|
||||
'.wall{position:absolute;inset:0;overflow-y:auto;padding:22px 18px 44px;' +
|
||||
// --wall-pad-top is read back by .sec-head::before to cover the scroll-padding strip
|
||||
// above a pinned header (see there). Declared as a var so the two can never drift:
|
||||
// typing 22px in both places is how that gap would silently re-open.
|
||||
'.wall{--wall-pad-top:22px;position:absolute;inset:0;overflow-y:auto;' +
|
||||
'padding:var(--wall-pad-top) 18px 44px;' +
|
||||
"background:#f6f7f9;color:#0f1419;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
|
||||
'.wall.is-dark{background:#0a1410;color:#e5e7eb}' +
|
||||
// Embedded: normal flow, no pane background, no pane padding. inset:auto matters as much
|
||||
// as position:static — leaving inset set keeps the box stretched even once static.
|
||||
'.wall--embedded{position:static;inset:auto;overflow-y:visible;padding:0;background:transparent}' +
|
||||
// ⚠ --wall-pad-top MUST go to 0 here too, not just the padding. Embedded, the wall is not
|
||||
// the scroll container — the HOST page is — so there is no scroll-padding strip to cover,
|
||||
// and a 22px cover bar would paint the wall's background over the host's own content
|
||||
// above each header. The var and the padding are one decision, set in one place.
|
||||
'.wall--embedded{--wall-pad-top:0px;position:static;inset:auto;overflow-y:visible;' +
|
||||
'padding:0;background:transparent}' +
|
||||
'.wall--embedded.is-dark{background:transparent}' +
|
||||
'.wall-h{margin:0 0 6px;font-size:19px;font-weight:700;text-align:center}' +
|
||||
'.wall-tag{margin:0 auto 12px;width:fit-content;padding:3px 11px;border-radius:999px;' +
|
||||
|
|
@ -497,11 +550,16 @@ useShadowStyles(
|
|||
// GAP must clear the OVERHANGING TAPES, not just the card boxes: the header tape
|
||||
// sits at top:-11px and the footer fieldset at bottom:-14px, so a 26px row gap
|
||||
// left ~1px of real air and the wall read as one dense block.
|
||||
// ⚠ THE ROW GAP MUST CLEAR THE PRINT ROW. .pfs hangs BELOW its card (top:100%, margin-top:-14px),
|
||||
// so the vertical gap is what keeps it off the next card. 64px was tuned when that row was one
|
||||
// line at 35px; wrapping it to two lines takes 78px and needs 78-14=64 — exactly the gap, with
|
||||
// nothing spare, which showed up as 5 rows intruding into the card below. 88px leaves real room.
|
||||
'.grid{display:grid;gap:88px 30px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));' +
|
||||
// ⚠ THE ROW GAP MUST CLEAR THE PRINT ROW, and 88px was tuned for a row that NO LONGER HANGS
|
||||
// THAT FAR. It was set when .pfs sat at top:100% with margin-top:-14px on the CARD — i.e. the
|
||||
// whole row (up to 78px wrapped) hung below the card and the gap had to clear all of it.
|
||||
// The row now straddles the BANNER's bottom edge and overhangs by --tape-overlap plus whatever
|
||||
// grows downward, and the card's own 26px bottom padding absorbs part of that. Reported from a
|
||||
// 390x844 phone shot: a ~180px empty band between one row's print stickers and the next row's
|
||||
// header stickers — the gap is now clearing a distance nothing occupies.
|
||||
// 56px still clears a two-line row (the ro/ru case) with room; the assertion that keeps this
|
||||
// honest is `neighbour 0` in check-poster-card-fits, which fails if a row reaches another card.
|
||||
'.grid{display:grid;gap:56px 30px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));' +
|
||||
'max-width:1180px;margin:0 auto}' +
|
||||
// The per-row control. Sits above its own grid, right-aligned so it reads as a setting for the
|
||||
// block below rather than as content, and inherits the wall's type so it does not announce
|
||||
|
|
@ -513,8 +571,13 @@ useShadowStyles(
|
|||
'border-radius:6px;background:#fff;color:inherit;cursor:pointer}' +
|
||||
'.is-dark .perrow__label{color:#9fb3a6}' +
|
||||
'.is-dark .perrow__select{background:#132019;border-color:rgba(110,231,183,.20)}' +
|
||||
'.card{position:relative;background:#fff;border:1px solid #e3e6ea;border-radius:3px;' +
|
||||
'padding:24px 12px 26px;box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
|
||||
// --card-pad-top IS the banner's top edge in card coordinates, and .tape-h reads it to
|
||||
// centre itself there. Declared as a var so the two cannot drift apart the way a second
|
||||
// hand-typed 24px would.
|
||||
'.card{--card-pad-top:24px;--tape-overlap:16px;position:relative;background:#fff;border:1px solid #e3e6ea;' +
|
||||
'border-radius:3px;' +
|
||||
'padding:var(--card-pad-top) 12px 26px;' +
|
||||
'box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
|
||||
'transition:transform .16s ease,box-shadow .16s ease}' +
|
||||
// ★ THE BLUE OUTLINE IS A HOVER STATE, NOT A MISSING STATIC BORDER. Several reports have asked
|
||||
// for "the blue outline around the poster" to be restored, and MEASURED 2026-09-24 there has
|
||||
|
|
@ -546,14 +609,21 @@ useShadowStyles(
|
|||
'font-size:10.5px;font-weight:600;letter-spacing:.02em;cursor:pointer;' +
|
||||
'border:1px solid rgba(0,0,0,.08);background:#eef2f6;color:#23303c}' +
|
||||
'.is-dark .tape{background:#1d2b23;color:#cfe9d9;border-color:rgba(255,255,255,.10)}' +
|
||||
// ★ SELF-CENTRING, like the footer. This was `top:-11px` -- half of the header's then-fixed
|
||||
// 22px height, so it straddled the top border only while that height never changed. The moment
|
||||
// the touch block below grows it to 24px that offset is wrong by a pixel, and it would be wrong
|
||||
// again for any locale with taller glyphs. `top:0` + translate(-50%,-50%) centres it on the
|
||||
// border at ANY height, which is the same mechanism the footer already uses and one less
|
||||
// hard-coded number to keep in sync. (.tape sets translateX; this is later in source, so it
|
||||
// wins -- both are 0-1-0.)
|
||||
'.tape-h{top:0;transform:translate(-50%,-50%);width:82%;padding:4px 8px;border-radius:999px;' +
|
||||
// ★ THE HEADER STICKER STRADDLES THE BANNER'S TOP EDGE, for the same reason the footer one
|
||||
// straddles the bottom. MEASURED before: 35px above the banner's top edge, -1px below — it
|
||||
// floated over the card, touching the banner nowhere. The Driver reads the rounded top
|
||||
// sticker as the one that "looks beautiful and is really well spaced", so this keeps its
|
||||
// size and shape exactly and only corrects WHERE it sits: centred on the edge it is taping.
|
||||
// --banner-pad-top reserves the banner surface it lands on, so it never reaches the map.
|
||||
// .tape-h is positioned against .card (it is a direct child), and the banner's top is the
|
||||
// card's padding-top — hence top:24px, read from the card rather than restated.
|
||||
// Same rule as .pfs below: the OVERLAP onto the banner is the constant, not the centre. This
|
||||
// sticker is a single line at every width so centring happened to work, but pinning both
|
||||
// stickers the same way means one --tape-overlap governs both and neither can drift.
|
||||
// Its height grows UPWARD here (bottom-anchored to the band) so it never reaches the map.
|
||||
'.tape-h{top:calc(var(--card-pad-top,24px) - var(--tape-overlap,16px));' +
|
||||
'transform:translateX(-50%);' +
|
||||
'width:82%;padding:4px 8px;border-radius:999px;' +
|
||||
'display:flex;align-items:center;justify-content:center;gap:5px}' +
|
||||
'.tape-h:hover{background:#e2e8ee}' +
|
||||
'.hcount{font-weight:800;font-variant-numeric:tabular-nums}' +
|
||||
|
|
@ -577,20 +647,43 @@ useShadowStyles(
|
|||
// a vertical one the driver saw. Anchoring at top:100% pins it just under the card, so the
|
||||
// second line grows DOWN into empty space. MEASURED: 92px between the preview's bottom and
|
||||
// the next card, of which the single-line row used 35 — 51px spare.
|
||||
// ★ THE FOOTER IS TAPE TOO (Driver): "see how the header for show on map is inline on the top of
|
||||
// the poster box? but the footer print options are not inline on the border or outline of the
|
||||
// box... we want the footer to imitate the header, by being inline like a piece of tape".
|
||||
// The header does that with .tape-h{top:-11px}: a fixed offset that works because its height is
|
||||
// fixed. The footer's height is NOT fixed -- it holds two to four controls plus a select, and it
|
||||
// grows with translated labels -- so a fixed margin-top:-14px left it hanging BELOW the edge
|
||||
// instead of sitting on it, and any height change moved it again.
|
||||
// translate(-50%,-50%) at top:100% centres it on the bottom border at ANY height, which is the
|
||||
// same "piece of tape" the header achieves and is self-correcting when a locale makes it taller.
|
||||
// That also answers the rest of the Driver's rule -- "if the box is too tall to centre we just
|
||||
// let it take up an overlap of the poster box, but not so far that it overlaps the poster blue
|
||||
// outline": half the tape overlaps the card and half hangs below, so the overlap grows only half
|
||||
// as fast as the tape does and never reaches the preview.
|
||||
'.pfs{position:absolute;top:100%;left:50%;transform:translate(-50%,-50%);' +
|
||||
// ★ THE FOOTER STICKER STRADDLES THE BANNER'S BOTTOM EDGE (Driver): "see how the header for
|
||||
// show on map is inline on the top of the poster box? but the footer print options are not
|
||||
// inline on the border or outline of the box... we want the footer to imitate the header, by
|
||||
// being inline like a piece of tape".
|
||||
//
|
||||
// top:100% + translate(-50%,-50%) centres it on that edge at ANY height, which matters because
|
||||
// the row's height is NOT fixed: it holds two to four controls and grows with translated
|
||||
// labels (MEASURED 68px in en/ro, 54px in zh). A fixed margin-top left it hanging below the
|
||||
// edge and moved again whenever the height changed. Centring is self-correcting, so no
|
||||
// constant here has to track the locale.
|
||||
//
|
||||
// ⚠ top:100% IS THE BANNER'S BOTTOM EDGE ONLY BECAUSE .pfs LIVES INSIDE .main. As a SIBLING
|
||||
// of the banner it resolved against the CARD instead — MEASURED card bottom 453 vs banner
|
||||
// bottom 426, so the row centred on a line 27px past the banner, touching it nowhere. The
|
||||
// re-parenting and the centring are one fix; neither works alone.
|
||||
//
|
||||
// Half the sticker lands on the banner, so the banner must reserve white space for it or the
|
||||
// sticker sits on the map: that is --banner-pad-bottom on .main, and the gate asserts the
|
||||
// result (map-covered 0) rather than trusting the arithmetic.
|
||||
//
|
||||
// ⚠ DO NOT ADD margin-top HERE. `margin:0` in the shorthand below RESETS any margin-top
|
||||
// declared above it — a shorthand always beats an earlier longhand in the same block. A
|
||||
// margin-top:-1px sat here computing to 0px while I believed it was doing the positioning;
|
||||
// a mutation test (set it to 26px, expect the row to move) proved it inert.
|
||||
// ⚠ NOT CENTRED — and this is a MEASURED correction, not a preference. translate(-50%,-50%)
|
||||
// puts HALF the row's height onto the banner, and the row's height is CONTENT-DEPENDENT: it
|
||||
// wraps to two lines in ro/ru and to three at 390px once the labels wrap. Measured ro/390px:
|
||||
// 27 of 27 stickers overlapping the map by 4px, while en and zh were clean at the same
|
||||
// padding. No fixed --banner-pad-bottom can be right for every locale x width, because half
|
||||
// of a variable is a variable.
|
||||
//
|
||||
// So the OVERLAP is the constant instead of the centre: the sticker's top is pinned a fixed
|
||||
// --tape-overlap above the banner's bottom edge, and any extra height grows DOWNWARD into
|
||||
// the grid's row gap, where there is room. The amount sitting on the banner is now the same
|
||||
// in all 11 locales at every width, which is what --banner-pad-bottom has to clear.
|
||||
'.pfs{position:absolute;top:calc(100% - var(--tape-overlap,16px));' +
|
||||
'left:50%;transform:translateX(-50%);' +
|
||||
// ⚠ min-width:0 ON THE FIELDSET ITSELF. A <fieldset> gets `min-width:min-content` from the
|
||||
// UA stylesheet, which OVERRIDES width and max-width — measured: computed width 431px
|
||||
// inside a 212px card while the rule said 82%. That is the real reason `width:82%` read as
|
||||
|
|
@ -606,13 +699,30 @@ useShadowStyles(
|
|||
// of 108 controls clipped. Fitting a row by hiding what its buttons say is not fitting it.
|
||||
// flex-basis:auto + wrap lets each control take the width its LABEL needs and flow to a
|
||||
// second line, which there is room for now that the row grows downward.
|
||||
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:nowrap}' +
|
||||
// ⚠ THE <select> IS THE ONE THAT WILL NOT SHRINK. Measured: with min-width:0 applied to
|
||||
// every child, .pstock still rendered 188px inside a 198px row because a <select> sizes to
|
||||
// its WIDEST OPTION and `flex:0 1 auto` keeps that as its basis. flex-basis:0 makes it share
|
||||
// the row like the buttons do; a cap keeps it from dominating the three text controls.
|
||||
'.pfs > .pstock{flex:0 1 auto;min-width:0;max-width:100%}' +
|
||||
// ⚠ nowrap HERE IS A CLIPPING BUG IN THE NARROW x LONG-LABEL CORNER, and it only appeared once
|
||||
// the per-row default became 2: at 390px that halves the card to ~170px, and Romanian's
|
||||
// "Pachet de cartonașe" (133px) plus two more controls cannot share it on one line.
|
||||
// MEASURED after the merge: 54 of 81 controls clipped at ro/390px, en and zh clean — long
|
||||
// labels and a narrow card, exactly the pair each change was individually fine with.
|
||||
// The standing ruling on this wall is that a label too long for its box WRAPS; nowrap forced
|
||||
// the browser to clip instead. Wrapping costs a second line in the one corner that needs it.
|
||||
'.pfs > .pbtn{flex:0 1 auto;min-width:0;white-space:normal;overflow-wrap:break-word;' +
|
||||
'text-align:center;line-height:1.2}' +
|
||||
'.is-dark .pfs{background:#132019;border-color:rgba(110,231,183,.24)}' +
|
||||
// ── THE REVEALED SIZE ROW ────────────────────────────────────────────────────
|
||||
// Sits directly under the print row, still hanging into the grid's 88px row-gap.
|
||||
// ⚠ THE OFFSET IS MEASURED, NOT GUESSED. Both rows share .main as containing block, so
|
||||
// "under .pfs" means top:100% + .pfs's own height — and that height is NOT a constant:
|
||||
// the row wraps to a second line in long-label locales (ro/ru measured at 2 lines).
|
||||
// A hard-coded 20px would tuck the size row under the print row in exactly those
|
||||
// locales. toggleDeck() reads the live offsetHeight and writes it to --pstocks-gap.
|
||||
// Hangs below the footer sticker, which is itself half-overhanging the banner edge — so the
|
||||
// offset starts at HALF the print row's height, not all of it.
|
||||
'.pstocks{top:100%;margin-top:calc(var(--pstocks-gap,20px) / 2);width:92%;max-width:92%;' +
|
||||
'box-sizing:border-box;padding:3px;border-radius:2px;background:#fff;' +
|
||||
'display:flex;flex-wrap:wrap;gap:3px;justify-content:center;' +
|
||||
'box-shadow:0 1px 4px rgba(0,0,0,.10);cursor:default}' +
|
||||
'.is-dark .pstocks{background:#132019;border-color:rgba(110,231,183,.24)}' +
|
||||
'.pfs > legend{margin:0 auto;padding:0 5px;font-size:8px;text-transform:uppercase;' +
|
||||
'letter-spacing:.6px;font-weight:700;color:#8a93a0}' +
|
||||
'.is-dark .pfs > legend{color:#7f9c8c}' +
|
||||
|
|
@ -623,13 +733,27 @@ useShadowStyles(
|
|||
'border-radius:2px;background:transparent;transition:background .15s}' +
|
||||
'.pbtn:hover{color:#10b981}' +
|
||||
'.pbtn:hover::after{background:#10b981}' +
|
||||
// Stock picker (cardStocks). Sized a shade smaller than .pbtn so it reads as a
|
||||
// SETTING for the neighbouring Card Deck button rather than a fourth action, and
|
||||
// `flex:0 1 auto` keeps it from taking an equal share of the row the way .pbtn does.
|
||||
'.pstock{flex:0 1 auto;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
|
||||
// The deck button is a DISCLOSURE, so it says so while open — a caret that flips,
|
||||
// plus the green the row already uses for "active", so the reader can see which
|
||||
// card's sizes are showing without reading aria-expanded.
|
||||
'.pbtn-deck[aria-expanded="true"]{color:#10b981}' +
|
||||
'.pbtn-deck::before{content:"\\25B8";display:inline-block;margin-right:3px;font-size:8px;' +
|
||||
'transition:transform .15s}' +
|
||||
'.pbtn-deck[aria-expanded="true"]::before{transform:rotate(90deg)}' +
|
||||
// Size choices. These are BUTTONS, not <option>s — the Driver ruled out a native
|
||||
// dropdown here because it collides with the other dropdowns on the host page.
|
||||
// Each one both picks the size and prints, so it is styled as an action.
|
||||
// ⚠ NO nowrap HERE, and this is a MEASURED correction rather than a preference. With
|
||||
// white-space:nowrap the two landscape labels rendered 161px inside a 169px row and CLIPPED
|
||||
// ("Card 3.5 x 2.5 in (landscape)", "Postcard 6 x 4 in (landscape)"). The standing ruling on
|
||||
// this wall is that a label too long for its box WRAPS — never an ellipsis, never a crop. So
|
||||
// each size takes a full row of its own when it needs one; flex-wrap on .pstocks gives it the
|
||||
// line, and min-width:0 lets the text actually break instead of forcing the box open.
|
||||
'.pstock{flex:0 1 auto;min-width:0;max-width:100%;border:1px solid #d8dde3;border-radius:3px;' +
|
||||
'background:#fff;color:#5b6672;font:inherit;font-size:9.5px;font-weight:600;' +
|
||||
'padding:2px 4px;cursor:pointer;line-height:1.25}' +
|
||||
'padding:2px 5px;cursor:pointer;line-height:1.25;white-space:normal;overflow-wrap:break-word}' +
|
||||
'.pstock:hover{border-color:#10b981;color:#10b981}' +
|
||||
'.pstock.is-on{border-color:#10b981;color:#0b7a5b;background:rgba(16,185,129,.10)}' +
|
||||
// ★ WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. MEASURED at 390x844 on the UPG-100
|
||||
// wall: .pbtn 76x20, .pstock 188x20, .tape-h 289x22 -- 135 controls under the minimum on one
|
||||
// phone screen, and the same components fail on three other maps because they live in library/.
|
||||
|
|
@ -638,8 +762,9 @@ useShadowStyles(
|
|||
// straddle the border without reaching the poster's blue outline, with ~9px of artwork clearance
|
||||
// left. Growing them for the mouse would spend 2px of that clearance to solve a problem a mouse
|
||||
// does not have, on a wall that is another owner's map. A finger gets the bigger target; the
|
||||
// desktop keeps the geometry that was signed off. Both tapes self-centre, so the row stays on
|
||||
// its border as it grows -- the overlap grows at half the rate of the height.
|
||||
// desktop keeps the geometry that was signed off. Safe to grow because both stickers are now
|
||||
// pinned by --tape-overlap rather than by their centres: the overlap onto the banner is the
|
||||
// constant, so extra height grows into empty space instead of walking over the artwork.
|
||||
// ⚠ STATE THE REQUIREMENT, DO NOT COMPUTE IT. The first attempt here grew the padding instead
|
||||
// -- 2px a side on .pbtn, 2px on .pstock -- and MEASURED 23.7px and 23.0px: under the minimum by
|
||||
// a third of a pixel and by one, because the height is padding PLUS a line box derived from a
|
||||
|
|
@ -650,7 +775,7 @@ useShadowStyles(
|
|||
'@media (pointer:coarse){' +
|
||||
'.pbtn{min-height:24px}' +
|
||||
'.pstock{min-height:24px}' +
|
||||
'.tape-h{min-height:24px;padding:5px 8px}' +
|
||||
'.tape-h{min-height:24px}' +
|
||||
'}' +
|
||||
// previewAction='print' only. The zoom-in cursor says "this opens something", and
|
||||
// the focus-visible outline is not optional: it is how a keyboard user sees where
|
||||
|
|
@ -694,6 +819,31 @@ useShadowStyles(
|
|||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.35);border-radius:4px 4px 0 0}' +
|
||||
'.is-dark .sec-head{border-bottom-color:rgba(255,255,255,.12);background:#0a1410;' +
|
||||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.7)}' +
|
||||
// ★ THE TRANSPARENT STRIP ABOVE A PINNED HEADER (Driver 2026-09-25): "there is a transparent
|
||||
// section above the header, so when you're scrolling the posters are visible above the header
|
||||
// ... in the white space between the header and the top of the page."
|
||||
//
|
||||
// CAUSE, MEASURED at 1440px: `position:sticky; top:0` pins to the scroll container's PADDING
|
||||
// BOX, not its border box. .wall has padding-top:22px, so a pinned header comes to rest 22px
|
||||
// down and the cards keep scrolling through those 22px. Probed at scrollTop 400/900/1600 the
|
||||
// element painting in that strip was IMG.img every time — literally posters flying past above
|
||||
// the header, exactly as reported.
|
||||
//
|
||||
// WHY A PSEUDO-ELEMENT AND NOT `top:-22px`. Pulling the header up by the padding does pin it
|
||||
// flush, but sticky clips nothing: the header's own first 22px would then sit outside the
|
||||
// container and its text would be cut off. And padding-top on the header itself would move the
|
||||
// text down instead of covering anything. A ::before that extends the SAME opaque fill upward
|
||||
// covers the strip while leaving both layout and the header's own position untouched — the
|
||||
// `outline` vs `border` distinction again: paint outside the box, do not re-lay-out.
|
||||
//
|
||||
// Height comes from --wall-pad-top (set on .wall), never a second literal 22px.
|
||||
// Full-bleed on x: the header is inset 2px by its own margin and the strip has to cover the
|
||||
// container's 18px side padding too, or the posters show through at the edges.
|
||||
'.sec-head::before{content:"";position:absolute;left:-20px;right:-20px;' +
|
||||
'bottom:100%;height:var(--wall-pad-top,0px);background:inherit;pointer-events:none}' +
|
||||
// Nothing to cover when the wall is not the scroller (--wall-pad-top is 0px there), but be
|
||||
// explicit: an embedded wall must never paint a bar over its host.
|
||||
'.wall--embedded .sec-head::before{display:none}' +
|
||||
// A collapsed row is a plain bar — no bottom rule reaching for content that is
|
||||
// not there.
|
||||
'.sec-head.is-shut{border-bottom-color:transparent;margin-bottom:6px}' +
|
||||
|
|
@ -713,7 +863,51 @@ useShadowStyles(
|
|||
'.sec-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums}' +
|
||||
// Poster aspect is 3ft x 3ft SQUARE — the real print ratio, so the wall previews
|
||||
// the actual artefact rather than an arbitrary box.
|
||||
'.main{position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:hidden;' +
|
||||
// ★ THE BANNER IS NOT THE MAP — and until now it effectively was.
|
||||
// Driver 2026-09-25, the sticker/olive-oil-bottle brief: this box is a BANNER that DISPLAYS
|
||||
// the map, the way a label displays a design. A banner taped to a wall gets a sticker at the
|
||||
// top and one at the bottom, each straddling its edge — "slight overlap over the banner, but
|
||||
// not over the content of the banner... if I put a piece of tape over the text it would be
|
||||
// offensive to the map."
|
||||
//
|
||||
// MEASURED before this change, and it is the whole problem: banner 240-426, map 241-425.
|
||||
// whiteSpaceTopOfBanner 1px, whiteSpaceBottomOfBanner 1px. The map filled the banner edge to
|
||||
// edge, so there was NO banner surface for a sticker to sit on — the bottom sticker could
|
||||
// only ever hang off into space (measured straddle: 1px on, 67px off) or cover the map.
|
||||
// Both wrong, and no amount of moving the sticker fixes it; the BANNER had to grow.
|
||||
//
|
||||
// --banner-pad is that room. .img is object-fit:contain, which fits the CONTENT box, so
|
||||
// padding reserves banner surface and the map shrinks to sit inside it rather than spilling.
|
||||
// The bottom gets MORE than the top because the footer sticker is taller than the header one
|
||||
// and, per the brief, should overhang the page BELOW the banner more than it covers the
|
||||
// banner itself ("probably an equal or more overlap").
|
||||
//
|
||||
// ⚠ aspect-ratio applies to the BORDER box here (box-sizing is border-box in this root), so
|
||||
// the square is the BANNER's shape, not the map's. The map inside is correspondingly shorter
|
||||
// than it is wide, which is correct: the banner is the thing being framed.
|
||||
//
|
||||
// overflow IS VISIBLE, DELIBERATELY. .pfs lives INSIDE this banner so its top:100% resolves
|
||||
// here; under `hidden` the print row was clipped to nothing. Nothing else needs the clip:
|
||||
// .img is object-fit:contain (never paints past its box) and .ph is a smaller flow box.
|
||||
// ⚠ DERIVED FROM THE STICKER HEIGHTS, NOT CHOSEN — and derived with SLACK, because the
|
||||
// heights are fractional. A sticker centred on the banner edge puts HALF its height onto the
|
||||
// banner, so the reserved white space must exceed that half.
|
||||
// attempt 1: 10px/26px by eye vs 34px/68px stickers -> map covered by 6px and 8px.
|
||||
// attempt 2: exactly half + 4px (21px/38px) -> STILL 1px of overlap on 27 cards in en and
|
||||
// ro but NOT zh, where the print row is 54px rather than 68px. The rects are
|
||||
// fractional (68.x), so half of them rounds past an integer constant; and the
|
||||
// row's height is LOCALE-DEPENDENT, which no fixed number can track.
|
||||
// Hence generous slack rather than a tighter constant: 12px past the widest measured half.
|
||||
// The gate asserts the relationship (map-covered 0), so if a future locale wraps the row to
|
||||
// three lines this fails loudly instead of quietly shaving the map.
|
||||
// Now that the OVERLAP is a constant (--tape-overlap on .card) rather than half a
|
||||
// content-dependent height, the banner only has to reserve that overlap plus daylight —
|
||||
// the same number in every locale at every width. It was 29/50 chasing half of a row that
|
||||
// is 68px in en, 54px in zh and taller still when ro wraps at 390px; that chase is what put
|
||||
// 4px of sticker on the map. 16 + 8 of daylight, both ends.
|
||||
'.main{--banner-pad-top:24px;--banner-pad-bottom:24px;' +
|
||||
'position:relative;aspect-ratio:1/1;border:1px solid #dfe3e7;overflow:visible;' +
|
||||
'padding:var(--banner-pad-top) 10px var(--banner-pad-bottom);' +
|
||||
'background:#fbfcfd;display:flex;align-items:center;justify-content:center}' +
|
||||
'.is-dark .main{background:#0c1712;border-color:rgba(255,255,255,.08)}' +
|
||||
// CONTAIN, not cover: `cover` crops to fill, which on a square sheet sliced off
|
||||
|
|
@ -827,26 +1021,44 @@ useShadowStyles(
|
|||
the two beside it. -->
|
||||
<div class="ph-note">preview unavailable</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="tape pfs">
|
||||
<legend>{{ t('buttons.printOptions') }}</legend>
|
||||
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
|
||||
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
|
||||
<!-- CARD DECK — only when this wall can supply rows. A button that
|
||||
cannot work is worse than an absent one, so it is gated on the
|
||||
records provider rather than shown and then failing at click. -->
|
||||
<button v-if="canPrintRows" type="button" class="pbtn"
|
||||
@click="prints.printCardDeck(item)">{{ t('buttons.cardDeck') }}</button>
|
||||
<!-- Stock picker, only where the wall offers one AND the deck is printable.
|
||||
@click.stop so choosing a size does not also fire the card's own
|
||||
show-on-map handler. Labels come from card-render.js STOCK. -->
|
||||
<select v-if="canPrintRows && stockOptions.length" class="pstock"
|
||||
v-model="pickedStock" :aria-label="t('buttons.cardStockAria')" @click.stop>
|
||||
<option v-for="s in stockOptions" :key="s.id" :value="s.id">{{ s.label }}</option>
|
||||
</select>
|
||||
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
||||
</fieldset>
|
||||
<!-- ★ THE PRINT ROW LIVES INSIDE .main, NOT BESIDE IT (Driver 2026-09-25).
|
||||
It is `position:absolute; top:100%`, which resolves against the nearest
|
||||
positioned ANCESTOR. As a SIBLING of .main that ancestor was .card, whose
|
||||
bottom sits 26px past the frame — measured: frame bottom 426, row top 452.
|
||||
That 26px of nothing is why it read as a detached row instead of tape.
|
||||
Moved INSIDE .main, top:100% is the frame's own bottom edge, so the row
|
||||
starts exactly on the border it is taping over. -->
|
||||
<fieldset class="tape pfs">
|
||||
<legend>{{ t('buttons.printOptions') }}</legend>
|
||||
<button v-if="!(hidePrayerListWhenSoon && !canPrintRows)" type="button" class="pbtn"
|
||||
@click="downloadPacket(item)">{{ t('buttons.prayerList') }}</button>
|
||||
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
||||
<!-- ★ CARD DECK IS NOT A PEER OPTION, AND ITS SIZE PICKER IS HIDDEN UNTIL ASKED
|
||||
FOR (Driver 2026-09-25): "card should not be second option, and I don't want
|
||||
to add the dropdown for the card because it makes it hard for others to have
|
||||
dropdowns too... only let you click on the dropdown when you click on print
|
||||
the cards, which will show you a hidden dropdown to let you choose the print
|
||||
size option you want."
|
||||
So the deck is a DISCLOSURE, not a sibling button: clicking it reveals this
|
||||
card's own size row, and the size buttons there are what actually print.
|
||||
A 162px <select> was the single widest control in a 172px row — removing it
|
||||
from the default row is most of why the row now fits on one line. -->
|
||||
<button v-if="canPrintRows" type="button" class="pbtn pbtn-deck"
|
||||
:aria-expanded="String(deckOpenFor === item.id)"
|
||||
@click.stop="toggleDeck(item.id, $event)">{{ t('buttons.cardDeck') }}</button>
|
||||
</fieldset>
|
||||
|
||||
<!-- The revealed size row. Rendered as SIBLINGS in a second tape rather than a
|
||||
<select> so there is no native dropdown to collide with other dropdowns —
|
||||
which is the constraint the Driver gave, not a styling preference. -->
|
||||
<div v-if="canPrintRows && stockOptions.length && deckOpenFor === item.id"
|
||||
class="tape pstocks" role="group" :aria-label="t('buttons.cardStockAria')">
|
||||
<button v-for="s in stockOptions" :key="s.id" type="button" class="pstock"
|
||||
:class="{ 'is-on': pickedStock === s.id }"
|
||||
@click.stop="printDeckAt(item, s.id)">{{ s.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -76,23 +76,74 @@ export function usePosterPrints(opts = {}) {
|
|||
// ── 1. THE POSTER ─────────────────────────────────────────────────────────
|
||||
// Prefers the pre-rendered image; falls back to rasterising the poster's own
|
||||
// HTML in the browser, which is what lets the repo stop shipping 41 MB of webp.
|
||||
// ★ THIS IS THE WALL'S posterAction='print' PATH, PROMOTED. It was a private function in
|
||||
// PosterWall.vue, which meant the MAP LEGEND's own "print this list's poster" had no way to
|
||||
// reach it — and the legend had grown a fourth implementation instead: a hand-rolled
|
||||
// `<a download>` of the .webp. That was wrong twice over. It silently downloads a file,
|
||||
// which the Driver ruled out explicitly (2026-09-23: "no silent file download as the default
|
||||
// action"), and it has NO fallback, so on the 27 lists whose .webp does not ship it simply
|
||||
// saved a 404.
|
||||
//
|
||||
// Two guards below were each learned the hard way and MUST survive any future move:
|
||||
// 1. a THROWN fetch is a MISS, not an error (blocked/offline/refused reject rather than
|
||||
// returning !ok). Letting it escape skips the rasterise fallback entirely — measured
|
||||
// during the wall migration, when check-poster-print-fallback went PASS -> FAIL with 0
|
||||
// .html requests.
|
||||
// 2. a blank raster must fail LOUDLY. An SVG inside an <img> is an isolated document, so a
|
||||
// missing stylesheet paints every fill black with no error at all.
|
||||
//
|
||||
// `assetsBase` is the seam: the wall and the map profile resolve their own base, and this
|
||||
// stays ignorant of where a bundle serves its posters from.
|
||||
async function printPoster(item) {
|
||||
if (busy()) return;
|
||||
const kind = 'Poster';
|
||||
const base = opts.assetsBase ? opts.assetsBase() : null;
|
||||
const id = item?.id ?? item;
|
||||
set(item, kind, 'fetch', 'fetching the poster', 25);
|
||||
try {
|
||||
const url = opts.posterUrl ? opts.posterUrl(item) : null;
|
||||
let html = '';
|
||||
if (url) {
|
||||
const res = await fetch(url, { cache: 'force-cache' });
|
||||
if (res.ok) html = await res.text();
|
||||
}
|
||||
if (!html) throw new Error('no poster available for this item');
|
||||
openPrintFrame(html, { autoprint: true });
|
||||
done(item, kind);
|
||||
// An explicit posterUrl still wins — it is how a caller points at a poster that is not
|
||||
// <base>/posters/<id>.webp at all.
|
||||
const direct = opts.posterUrl ? opts.posterUrl(item) : null;
|
||||
let res = null;
|
||||
const webp = direct || (base ? `${base}/posters/${id}.webp` : null);
|
||||
if (webp) { try { res = await fetch(webp); } catch { /* a miss, see guard 1 */ } }
|
||||
if (res && res.ok) { await deliverPoster(await res.blob(), item, kind); return; }
|
||||
if (!base) throw new Error('no poster available for this item');
|
||||
// NO PRE-RENDERED IMAGE? GENERATE IT. The .webp posters are ~41MB across a tree and are
|
||||
// not shipped by default; the HTML is the SOURCE and is smaller than the image it makes.
|
||||
set(item, kind, 'render', 'generating the poster', 45);
|
||||
const htmlRes = await fetch(`${base}/posters/${id}.html`);
|
||||
if (!htmlRes.ok) throw new Error(`poster not found (${htmlRes.status})`);
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js');
|
||||
const blob = await rasterPoster(await htmlRes.text(), { scale: 2 });
|
||||
if (!(await looksRendered(blob))) throw new Error('the generated poster rendered blank');
|
||||
await deliverPoster(blob, item, kind);
|
||||
} catch (e) { fail(item, kind, e); }
|
||||
}
|
||||
|
||||
// A 3ft SQUARE on a PORTRAIT sheet must fit by its SHORTER axis: scaling to the full page
|
||||
// WIDTH makes it taller than the printable height and it overflows onto a second page.
|
||||
// Constrain both axes and let object-fit letterbox it.
|
||||
const posterObjectUrls = [];
|
||||
async function deliverPoster(blob, item, kind) {
|
||||
set(item, kind, 'save', 'opening the print dialog', 80);
|
||||
const url = URL.createObjectURL(blob);
|
||||
posterObjectUrls.push(url);
|
||||
const title = `${opts.downloadPrefix || 'doxa'} — ${label(item)}`;
|
||||
openPrintFrame(
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title>'
|
||||
+ '<style>@page{size:letter portrait;margin:12mm}'
|
||||
+ 'html,body{margin:0;height:100%;background:#fff}'
|
||||
+ 'body{display:flex;align-items:center;justify-content:center}'
|
||||
+ 'img{max-width:100%;max-height:100vh;object-fit:contain;display:block}'
|
||||
+ '@media print{body{height:auto}'
|
||||
+ 'img{width:auto;max-width:100%;max-height:100%;object-fit:contain}}</style></head>'
|
||||
+ `<body><img src="${url}" alt="${esc(label(item))}"></body></html>`,
|
||||
{ autoprint: true },
|
||||
);
|
||||
done(item, kind);
|
||||
}
|
||||
|
||||
// ── 2. THE LIST ───────────────────────────────────────────────────────────
|
||||
// The Driver's FIRST option — "that's what the whole app was about": the list
|
||||
// itself as a print-paginated table with a prayer checkbox per row.
|
||||
|
|
|
|||
|
|
@ -93,34 +93,6 @@ export function detectLocale() {
|
|||
/**
|
||||
* Build a fresh i18n instance. Called once per <doxa-map> element.
|
||||
*/
|
||||
/**
|
||||
* Read a catalogue string WITHOUT the vue-i18n plugin.
|
||||
*
|
||||
* WHY THIS EXISTS. Some components are mounted as their OWN custom element by a bundle entry that
|
||||
* calls defineCustomElement(Component) and installs nothing — people-groups-grid is one. useI18n()
|
||||
* inside such a component THROWS, and a thrown setup() removes the subtree silently (this repo has
|
||||
* already paid for that once: three buttons vanished behind ignorable console noise). So a shared
|
||||
* component that must work in BOTH mounts needs a plugin-free reader, which is all this is.
|
||||
*
|
||||
* Falls back locale -> en -> undefined, so a caller can keep its own literal as the last resort and
|
||||
* a missing translation degrades to English rather than to a key.
|
||||
*
|
||||
* @param {string} path dotted, e.g. 'grid.hide_filters'
|
||||
* @param {string} [locale] defaults to the detected one (?lang= / host / browser)
|
||||
*/
|
||||
export function tStatic(path, locale) {
|
||||
const tryTree = (loc) => {
|
||||
let node = messages[loc]
|
||||
for (const part of String(path).split('.')) {
|
||||
if (!node || typeof node !== 'object') return undefined
|
||||
node = node[part]
|
||||
}
|
||||
return typeof node === 'string' ? node : undefined
|
||||
}
|
||||
const want = locale || detectLocale()
|
||||
return tryTree(want) ?? tryTree('en')
|
||||
}
|
||||
|
||||
export function createAppI18n() {
|
||||
const locale = detectLocale()
|
||||
// ⚠ REFLECT AN RTL LOCALE ON THE DOCUMENT. Only one component reads RTL_LOCALES
|
||||
|
|
@ -149,3 +121,31 @@ export function createAppI18n() {
|
|||
}
|
||||
|
||||
export default createAppI18n
|
||||
|
||||
/**
|
||||
* Read a catalogue string WITHOUT the vue-i18n plugin.
|
||||
*
|
||||
* WHY THIS EXISTS. Some components are mounted as their OWN custom element by a bundle entry that
|
||||
* calls defineCustomElement(Component) and installs nothing — people-groups-grid is one. useI18n()
|
||||
* inside such a component THROWS, and a thrown setup() removes the subtree silently (this repo has
|
||||
* already paid for that once: three buttons vanished behind ignorable console noise). So a shared
|
||||
* component that must work in BOTH mounts needs a plugin-free reader, which is all this is.
|
||||
*
|
||||
* Falls back locale -> en -> undefined, so a caller can keep its own literal as the last resort and
|
||||
* a missing translation degrades to English rather than to a key.
|
||||
*
|
||||
* @param {string} path dotted, e.g. 'grid.hide_filters'
|
||||
* @param {string} [locale] defaults to the detected one (?lang= / host / browser)
|
||||
*/
|
||||
export function tStatic(path, locale) {
|
||||
const tryTree = (loc) => {
|
||||
let node = messages[loc]
|
||||
for (const part of String(path).split('.')) {
|
||||
if (!node || typeof node !== 'object') return undefined
|
||||
node = node[part]
|
||||
}
|
||||
return typeof node === 'string' ? node : undefined
|
||||
}
|
||||
const want = locale || detectLocale()
|
||||
return tryTree(want) ?? tryTree('en')
|
||||
}
|
||||
|
|
|
|||
13
library/i18n/locales/ar/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "كل مجموعات الشعوب",
|
||||
"locations": "{n} موقعًا (مجموعة × بلد)",
|
||||
"doxa": "مجموعات شعوب DOXA",
|
||||
"frontier": "حدودية — لم يبدأ شيء",
|
||||
"unreached": "غير مُبشَّرة — العمل يبدأ",
|
||||
"reached": "مُبشَّرة — الكنيسة موجودة",
|
||||
"hint": "انقر على صف لعرضها كلها · انقر مرة أخرى لعرض الكل",
|
||||
"loading": "جارٍ تحميل كل مجموعات الشعوب…",
|
||||
"loadError": "تعذّر تحميل الخريطة.",
|
||||
"pray": "صلِّ لأجلهم ←",
|
||||
"population": "السكان {n}"
|
||||
}
|
||||
32
library/i18n/locales/ar/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "أدوات البحث",
|
||||
"clustering": "التجميع",
|
||||
"filters": "عوامل التصفية",
|
||||
"pin_dim": "خفوت النقاط",
|
||||
"export_poster": "تصدير ملصق…",
|
||||
"reload_data": "إعادة تحميل البيانات",
|
||||
"off": "إيقاف",
|
||||
"native": "أصلي",
|
||||
"mst": "الشجرة الممتدة",
|
||||
"network": "شبكة",
|
||||
"off_desc": "إظهار كل نقطة على حدة",
|
||||
"native_desc": "تجميع دوائر Mapbox (الأسرع)",
|
||||
"mst_desc": "شبكات تقارب بالشجرة الممتدة الصغرى",
|
||||
"network_desc": "خطوط ربط بأقرب جار",
|
||||
"search_languages": "ابحث عن اللغات…",
|
||||
"search_religions": "ابحث عن الأديان…",
|
||||
"reset_all": "إعادة تعيين الكل",
|
||||
"no_languages": "لم تُحمَّل أي لغات بعد",
|
||||
"no_religions": "لم تُحمَّل أي أديان بعد",
|
||||
"unadopted": "غير متبنّى",
|
||||
"min": "الأدنى",
|
||||
"max": "الأعلى",
|
||||
"adopted": "متبنَّاة",
|
||||
"adoption": "التبني",
|
||||
"engaged": "مُخْدُومَة",
|
||||
"engagement": "المشاركة",
|
||||
"unengaged": "يحتاج إلى انخراط",
|
||||
"language": "اللغة",
|
||||
"religion": "الدين",
|
||||
"population": "السكان"
|
||||
}
|
||||
35
library/i18n/locales/ar/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "كتلة التقارب",
|
||||
"gospel_access": "الوصول إلى الإنجيل",
|
||||
"people": "الشعوب",
|
||||
"people_name": "الشعب (الاسم)",
|
||||
"praying": "يصلّون",
|
||||
"religion_family": "عائلة الأديان",
|
||||
"search_language": "ابحث عن لغة",
|
||||
"search_people": "ابحث عن شعب",
|
||||
"search_place": "ابحث عن مكان",
|
||||
"search_religion": "ابحث عن دين",
|
||||
"upgs_in_scope": "الشعوب ضمن النطاق",
|
||||
"countries_touched": "الدول المشمولة",
|
||||
"unreached": "غير موصولين",
|
||||
"sum_population": "مجموع السكان",
|
||||
"sum_praying": "مجموع المصلّين",
|
||||
"charts": "الرسوم البيانية",
|
||||
"table": "الجدول",
|
||||
"cluster": "المجموعة",
|
||||
"country": "الدولة",
|
||||
"engagement": "المشاركة",
|
||||
"language": "اللغة",
|
||||
"language_family": "عائلة اللغة",
|
||||
"people_group": "الشعب",
|
||||
"population": "السكان",
|
||||
"prayer": "الصلاة",
|
||||
"region": "المنطقة",
|
||||
"religion": "الدين",
|
||||
"status": "الحالة",
|
||||
"adopted": "متبنَّاة",
|
||||
"filters": "عوامل التصفية",
|
||||
"gospel_resources": "موارد الإنجيل",
|
||||
"map": "الخريطة",
|
||||
"search_placeholder": "ابحث عن الشعوب والأماكن واللغات والأديان…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "لغة غير صحيحة",
|
||||
"varietyNotStated": "(اللهجة غير محددة)",
|
||||
"inLanguage": "بـ"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "كل نقطة هي شعب واحد من شعوب دوكسا ولغته؛ أما لون البلد فهو عدد تلك الشعوب.",
|
||||
"noneFoundShort": "«لا شيء» تعني غير موجود في المصادر التي بحثنا فيها — وليست دليلًا على عدم وجوده.",
|
||||
"tapCountry": "اضغط على بلد لترى الموارد الفعلية، شعبًا شعبًا."
|
||||
},
|
||||
"tables": {
|
||||
"title": "موارد الإنجيل — البيانات",
|
||||
"showing": "عرض",
|
||||
"of": "من",
|
||||
"for": "لـ",
|
||||
"filteredTo": "مصفّى حسب",
|
||||
"showAll": "عرض كل الموارد",
|
||||
"t_doxa": "دوكسا — الوصول إلى الإنجيل",
|
||||
"t_links": "كل مورد ورابطه",
|
||||
"t_nothing": "حيث لا يوجد شيء",
|
||||
"t_countrygap": "الفجوة حسب البلد",
|
||||
"t_translation": "يسمعون ولا نص لديهم",
|
||||
"t_translationcountry": "أين نترجم",
|
||||
"t_gaps": "الفجوات والفرص",
|
||||
"t_bridges": "جسور المدن",
|
||||
"t_deaf": "فجوة الصم",
|
||||
"t_resources": "الموارد حسب الشعب",
|
||||
"t_languages": "اللغات حسب البلد",
|
||||
"t_growth": "الوصول مقابل نمو السكان"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/ar/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "ابحث عن الشعوب…",
|
||||
"hide_filters": "إخفاء عوامل التصفية",
|
||||
"show_filters": "إظهار عوامل التصفية",
|
||||
"total": "الإجمالي",
|
||||
"showing": "المعروض",
|
||||
"load_more": "عرض المزيد",
|
||||
"full_profile": "الملف الكامل",
|
||||
"wagf_region": "المنطقة",
|
||||
"wagf_block": "كتلة التقارب",
|
||||
"rop1": "الشعب",
|
||||
"religion": "الدين",
|
||||
"exact_filter": "تطابق تام",
|
||||
"clear_all": "مسح الكل",
|
||||
"loading": "جارٍ التحميل…",
|
||||
"no_results": "لا توجد شعوب مطابقة لهذا التصفية.",
|
||||
"type_to_search": "اكتب للبحث...",
|
||||
"load_failed": "تعذّر تحميل بيانات الشعوب. هذه مشكلة اتصال، وليست مشكلة في التصفية."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "القائمة قادمة قريباً",
|
||||
"pinHint": {
|
||||
"tapDot": "اضغط على نقطة ملوّنة — لا يزال هناك من يحتاج إلى الصلاة"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
|
||||
"upg100WallTag": "تعبئة للصلاة",
|
||||
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "الدولة",
|
||||
"upg100Region": "المنطقة",
|
||||
"upg100Zone": "قوائم UPG-100",
|
||||
"upg100Group": "مجموعة شعبية"
|
||||
}
|
||||
"upg100Group": "مجموعة شعبية",
|
||||
"selectView": "اختر العرض",
|
||||
"views": "العروض",
|
||||
"prayerResources": "موارد الصلاة",
|
||||
"map": "الخريطة"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/ar/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · الرؤية",
|
||||
"title": "كل شعب. كل مكان.",
|
||||
"tagline": "من كل مجموعات الشعوب في العالم إلى التي تصلي لأجلها DOXA، محسوبةً من البيانات.",
|
||||
"funnelLabel": "من كل مجموعات الشعوب إلى مجموعات شعوب DOXA",
|
||||
"allLabel": "مجموعات الشعوب",
|
||||
"allNote": "كل مجموعة شعب، في كل بلد تعيش فيه",
|
||||
"unreachedLabel": "غير المُبشَّرة",
|
||||
"unreachedNote": "أقل من 2% إنجيليون",
|
||||
"frontierLabel": "الحدودية",
|
||||
"frontierNote": "أقل من 0.1% إنجيليون",
|
||||
"doxaLabel": "مجموعات شعوب DOXA",
|
||||
"doxaNote": "{frontier} منها حدودية",
|
||||
"people": "{n} نسمة",
|
||||
"sources": "مجموعات الشعوب: {imb}، بيانات {date}. قائمة DOXA: pray.doxa.life، {date2}.",
|
||||
"howCounted": "كيف تُحسب هذه الأرقام",
|
||||
"ruleAll": "كل صف في بيانات IMB لمجموعات الشعوب: مجموعة شعب واحدة في بلد واحد.",
|
||||
"ruleUnreached": "أولوية الضياع لدى IMB من 0 إلى 2: أقل من 2% إنجيليون.",
|
||||
"ruleFrontier": "أولوية الضياع لدى IMB رقم 0: أقل من 0.1% إنجيليون. جزء من غير المُبشَّرة.",
|
||||
"ruleDoxa": "كل مجموعة في قائمة صلاة DOXA، مطابَقة مع بيانات IMB حسب الشعب والبلد. ليست كلها حدودية."
|
||||
}
|
||||
13
library/i18n/locales/de/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Alle Volksgruppen",
|
||||
"locations": "{n} Standorte (Gruppe × Land)",
|
||||
"doxa": "DOXA-Volksgruppen",
|
||||
"frontier": "Frontier — noch nichts begonnen",
|
||||
"unreached": "Unerreicht — Arbeit beginnt",
|
||||
"reached": "Erreicht — Gemeinde ist da",
|
||||
"hint": "Zeile anklicken, um alle zu sehen · erneut klicken für alle",
|
||||
"loading": "Alle Volksgruppen werden geladen…",
|
||||
"loadError": "Die Karte konnte nicht geladen werden.",
|
||||
"pray": "Für sie beten →",
|
||||
"population": "Einw. {n}"
|
||||
}
|
||||
32
library/i18n/locales/de/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Forschungssteuerung",
|
||||
"clustering": "Clustering",
|
||||
"filters": "Filter",
|
||||
"pin_dim": "Pin-Transparenz",
|
||||
"export_poster": "Poster exportieren…",
|
||||
"reload_data": "Daten neu laden",
|
||||
"off": "Aus",
|
||||
"native": "Nativ",
|
||||
"mst": "MST",
|
||||
"network": "Netzwerk",
|
||||
"off_desc": "Jeden Pin einzeln anzeigen",
|
||||
"native_desc": "Mapbox-Kreiscluster (am schnellsten)",
|
||||
"mst_desc": "Affinitätsnetze über minimalen Spannbaum",
|
||||
"network_desc": "Verbindungslinien zum nächsten Nachbarn",
|
||||
"search_languages": "Sprachen suchen…",
|
||||
"search_religions": "Religionen suchen…",
|
||||
"reset_all": "Alles zurücksetzen",
|
||||
"no_languages": "Noch keine Sprachen geladen",
|
||||
"no_religions": "Noch keine Religionen geladen",
|
||||
"unadopted": "Nicht adoptiert",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"adopted": "Adoptiert",
|
||||
"adoption": "Adoption",
|
||||
"engaged": "Engagiert",
|
||||
"engagement": "Engagement",
|
||||
"unengaged": "Braucht Engagement",
|
||||
"language": "Sprache",
|
||||
"religion": "Religion",
|
||||
"population": "Bevölkerung"
|
||||
}
|
||||
35
library/i18n/locales/de/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Affinitätsblock",
|
||||
"gospel_access": "Zugang zum Evangelium",
|
||||
"people": "Völker",
|
||||
"people_name": "Volk (Name)",
|
||||
"praying": "Betend",
|
||||
"religion_family": "Religionsfamilie",
|
||||
"search_language": "Sprache suchen",
|
||||
"search_people": "Volk suchen",
|
||||
"search_place": "Ort suchen",
|
||||
"search_religion": "Religion suchen",
|
||||
"upgs_in_scope": "UPGs im Bereich",
|
||||
"countries_touched": "berührte Länder",
|
||||
"unreached": "unerreicht",
|
||||
"sum_population": "Σ Bevölkerung",
|
||||
"sum_praying": "Σ Betende",
|
||||
"charts": "Diagramme",
|
||||
"table": "Tabelle",
|
||||
"cluster": "Cluster",
|
||||
"country": "Land",
|
||||
"engagement": "Engagement",
|
||||
"language": "Sprache",
|
||||
"language_family": "Sprachfamilie",
|
||||
"people_group": "Volk",
|
||||
"population": "Bevölkerung",
|
||||
"prayer": "Gebet",
|
||||
"region": "Region",
|
||||
"religion": "Religion",
|
||||
"status": "Status",
|
||||
"adopted": "Adoptiert",
|
||||
"filters": "Filter",
|
||||
"gospel_resources": "Evangeliumsressourcen",
|
||||
"map": "Karte",
|
||||
"search_placeholder": "Volksgruppen, Orte, Sprachen, Religionen suchen…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "falsche Sprache",
|
||||
"varietyNotStated": "(Varietät nicht angegeben)",
|
||||
"inLanguage": "auf"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "Ein Punkt ist EINE Volksgruppe von Doxa mit ihrer eigenen Sprache; ein Land ist die ANZAHL dieser Gruppen.",
|
||||
"noneFoundShort": "„Nichts gefunden“ heißt: nicht in den geprüften Quellen — nie ein Beweis, dass es nichts gibt.",
|
||||
"tapCountry": "Tippen Sie auf ein Land, um die Ressourcen Gruppe für Gruppe zu sehen."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Evangeliums-Ressourcen — Daten",
|
||||
"showing": "Zeige",
|
||||
"of": "von",
|
||||
"for": "für",
|
||||
"filteredTo": "Gefiltert nach",
|
||||
"showAll": "Alle Ressourcen zeigen",
|
||||
"t_doxa": "Doxa — Zugang zum Evangelium",
|
||||
"t_links": "Jede Ressource + Link",
|
||||
"t_nothing": "Wo nichts gefunden wurde",
|
||||
"t_countrygap": "Die Lücke nach Land",
|
||||
"t_translation": "Hören ja, Text nein",
|
||||
"t_translationcountry": "Wo übersetzen",
|
||||
"t_gaps": "Lücken & Chancen",
|
||||
"t_bridges": "Stadt-Brücken",
|
||||
"t_deaf": "Die Gehörlosen-Lücke",
|
||||
"t_resources": "Ressourcen nach Volksgruppe",
|
||||
"t_languages": "Sprachen nach Land",
|
||||
"t_growth": "Zugang vs. Bevölkerungswachstum"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/de/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "Volksgruppen suchen…",
|
||||
"hide_filters": "Filter ausblenden",
|
||||
"show_filters": "Filter anzeigen",
|
||||
"total": "Gesamt",
|
||||
"showing": "Angezeigt",
|
||||
"load_more": "Mehr laden",
|
||||
"full_profile": "Vollständiges Profil",
|
||||
"wagf_region": "Region",
|
||||
"wagf_block": "Affinitätsblock",
|
||||
"rop1": "Volk",
|
||||
"religion": "Religion",
|
||||
"exact_filter": "Exakt",
|
||||
"clear_all": "Alle löschen",
|
||||
"loading": "Wird geladen…",
|
||||
"no_results": "Keine Volksgruppen entsprechen diesem Filter.",
|
||||
"type_to_search": "Zum Suchen tippen...",
|
||||
"load_failed": "Die Daten konnten nicht geladen werden. Das ist ein Verbindungsproblem, nicht Ihr Filter."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menü folgt in Kürze",
|
||||
"pinHint": {
|
||||
"tapDot": "Tippe auf einen farbigen Punkt — jemand braucht noch Gebet"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Meine UPG-100-Liste — Gebetslisten und Poster",
|
||||
"upg100WallTag": "Gebetsmobilisierung",
|
||||
"upg100WallIntro": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie <strong>mit dem Namen ihrer Volksgruppe</strong> beten."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Land",
|
||||
"upg100Region": "Region",
|
||||
"upg100Zone": "UPG-100-Listen",
|
||||
"upg100Group": "Volksgruppe"
|
||||
}
|
||||
"upg100Group": "Volksgruppe",
|
||||
"selectView": "Ansicht wählen",
|
||||
"views": "Ansichten",
|
||||
"prayerResources": "Gebetsressourcen",
|
||||
"map": "Karte"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/de/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · Vision",
|
||||
"title": "Jedes Volk. Jeder Ort.",
|
||||
"tagline": "Von allen Volksgruppen der Welt bis zu denen, für die DOXA betet, aus den Daten gezählt.",
|
||||
"funnelLabel": "Von allen Volksgruppen zu den DOXA-Volksgruppen",
|
||||
"allLabel": "Volksgruppen",
|
||||
"allNote": "jede Volksgruppe in jedem Land, in dem sie lebt",
|
||||
"unreachedLabel": "Unerreicht",
|
||||
"unreachedNote": "unter 2 % Evangelikale",
|
||||
"frontierLabel": "Frontier",
|
||||
"frontierNote": "unter 0,1 % Evangelikale",
|
||||
"doxaLabel": "DOXA-Volksgruppen",
|
||||
"doxaNote": "{frontier} davon sind Frontier",
|
||||
"people": "{n} Menschen",
|
||||
"sources": "Volksgruppen: {imb}, Datenstand {date}. DOXA-Liste: pray.doxa.life, {date2}.",
|
||||
"howCounted": "So wird gezählt",
|
||||
"ruleAll": "Jede Zeile der IMB-Volksgruppendaten: eine Volksgruppe in einem Land.",
|
||||
"ruleUnreached": "IMB-Lostness-Priorität 0 bis 2: weniger als 2 % Evangelikale.",
|
||||
"ruleFrontier": "IMB-Lostness-Priorität 0: weniger als 0,1 % Evangelikale. Eine Teilmenge der Unerreichten.",
|
||||
"ruleDoxa": "Jede Gruppe auf der DOXA-Gebetsliste, den IMB-Daten nach Volk und Land zugeordnet. Nicht alle sind Frontier."
|
||||
}
|
||||
13
library/i18n/locales/en/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "All People Groups",
|
||||
"locations": "{n} group–country locations",
|
||||
"doxa": "DOXA people groups",
|
||||
"frontier": "Frontier — nothing started",
|
||||
"unreached": "Unreached — work beginning",
|
||||
"reached": "Reached — church is there",
|
||||
"hint": "Click a row to see all of them · click again for everything",
|
||||
"loading": "Loading every people group…",
|
||||
"loadError": "The map could not be loaded.",
|
||||
"pray": "Pray for them →",
|
||||
"population": "pop {n}"
|
||||
}
|
||||
32
library/i18n/locales/en/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Research controls",
|
||||
"clustering": "Clustering",
|
||||
"filters": "Filters",
|
||||
"pin_dim": "Pin dim",
|
||||
"export_poster": "Export poster…",
|
||||
"reload_data": "Reload data",
|
||||
"off": "Off",
|
||||
"native": "Native",
|
||||
"mst": "MST",
|
||||
"network": "Network",
|
||||
"off_desc": "Show every pin individually",
|
||||
"native_desc": "Mapbox circle clusters (fastest)",
|
||||
"mst_desc": "Minimum-spanning-tree affinity webs",
|
||||
"network_desc": "Nearest-neighbor connection lines",
|
||||
"search_languages": "Search languages…",
|
||||
"search_religions": "Search religions…",
|
||||
"reset_all": "Reset all",
|
||||
"no_languages": "No languages loaded yet",
|
||||
"no_religions": "No religions loaded yet",
|
||||
"unadopted": "Unadopted",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"adopted": "Adopted",
|
||||
"adoption": "Adoption",
|
||||
"engaged": "Engaged",
|
||||
"engagement": "Engagement",
|
||||
"unengaged": "Unengaged",
|
||||
"language": "Language",
|
||||
"religion": "Religion",
|
||||
"population": "Population"
|
||||
}
|
||||
35
library/i18n/locales/en/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Bloc",
|
||||
"gospel_access": "Gospel Access",
|
||||
"people": "People",
|
||||
"people_name": "People (name)",
|
||||
"praying": "Praying",
|
||||
"religion_family": "Religion Family",
|
||||
"search_language": "Search language",
|
||||
"search_people": "Search people",
|
||||
"search_place": "Search place",
|
||||
"search_religion": "Search religion",
|
||||
"upgs_in_scope": "UPGs in scope",
|
||||
"countries_touched": "countries touched",
|
||||
"unreached": "unreached",
|
||||
"sum_population": "Σ population",
|
||||
"sum_praying": "Σ praying",
|
||||
"charts": "Charts",
|
||||
"table": "Table",
|
||||
"cluster": "Cluster",
|
||||
"country": "Country",
|
||||
"engagement": "Engagement",
|
||||
"language": "Language",
|
||||
"language_family": "Language Family",
|
||||
"people_group": "People Group",
|
||||
"population": "Population",
|
||||
"prayer": "Prayer",
|
||||
"region": "Region",
|
||||
"religion": "Religion",
|
||||
"status": "Status",
|
||||
"adopted": "Adopted",
|
||||
"filters": "Filters",
|
||||
"gospel_resources": "Gospel Resources",
|
||||
"map": "Map",
|
||||
"search_placeholder": "Search people groups, place, language, religion…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "wrong language",
|
||||
"varietyNotStated": "(variety not stated)",
|
||||
"inLanguage": "in"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "A dot is ONE of Doxa's people groups and its own language; a country is a COUNT of those groups.",
|
||||
"noneFoundShort": "“None found” means not present in the pools this build checked — never proof that nothing exists.",
|
||||
"tapCountry": "Tap a country to see the actual resources, group by group."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Gospel Resources — Data",
|
||||
"showing": "Showing",
|
||||
"of": "of",
|
||||
"for": "for",
|
||||
"filteredTo": "Filtered to",
|
||||
"showAll": "Show all resources",
|
||||
"t_doxa": "Doxa — Gospel Access",
|
||||
"t_links": "Every Resource + Link",
|
||||
"t_nothing": "Where Nothing Was Found",
|
||||
"t_countrygap": "The Gap by Country",
|
||||
"t_translation": "Can Hear It, No Text",
|
||||
"t_translationcountry": "Where to Translate",
|
||||
"t_gaps": "Gaps & Opportunities",
|
||||
"t_bridges": "City Bridges",
|
||||
"t_deaf": "The Deaf Gap",
|
||||
"t_resources": "Resources by People Group",
|
||||
"t_languages": "Languages by Country",
|
||||
"t_growth": "Access vs Population Growth"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/en/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "Search people groups…",
|
||||
"hide_filters": "Hide Filters",
|
||||
"show_filters": "Show Filters",
|
||||
"total": "Total",
|
||||
"showing": "Showing",
|
||||
"load_more": "Load more",
|
||||
"full_profile": "Full profile",
|
||||
"wagf_region": "Region",
|
||||
"wagf_block": "Affinity Block",
|
||||
"rop1": "People Group",
|
||||
"religion": "Religion",
|
||||
"exact_filter": "Exact",
|
||||
"clear_all": "Clear All",
|
||||
"loading": "Loading…",
|
||||
"no_results": "No people groups match this filter.",
|
||||
"type_to_search": "Type to search...",
|
||||
"load_failed": "People-group data could not be loaded. This is a connection problem, not your filter."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu coming soon",
|
||||
"pinHint": {
|
||||
"tapDot": "Tap a coloured dot — someone still needs prayer"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "My UPG 100 List — Prayer Lists & Posters",
|
||||
"upg100WallTag": "Prayer Mobilization",
|
||||
"upg100WallIntro": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them <strong>by their people group name</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Country",
|
||||
"upg100Region": "Region",
|
||||
"upg100Zone": "UPG-100 Lists",
|
||||
"upg100Group": "People Group"
|
||||
}
|
||||
"upg100Group": "People Group",
|
||||
"selectView": "Select View",
|
||||
"views": "Views",
|
||||
"prayerResources": "Prayer Resources",
|
||||
"map": "Map"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/en/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · Vision",
|
||||
"title": "Every people. Every place.",
|
||||
"tagline": "From every people group on earth to the ones DOXA prays for, counted from the data.",
|
||||
"funnelLabel": "From all people groups to DOXA people groups",
|
||||
"allLabel": "People groups",
|
||||
"allNote": "each people group, in each country it lives in",
|
||||
"unreachedLabel": "Unreached",
|
||||
"unreachedNote": "under 2% evangelical",
|
||||
"frontierLabel": "Frontier",
|
||||
"frontierNote": "under 0.1% evangelical",
|
||||
"doxaLabel": "DOXA people groups",
|
||||
"doxaNote": "{frontier} of them are frontier",
|
||||
"people": "{n} people",
|
||||
"sources": "People groups: {imb}, data of {date}. DOXA list: pray.doxa.life, {date2}.",
|
||||
"howCounted": "How these are counted",
|
||||
"ruleAll": "Every row of the IMB people-group data: one people group in one country.",
|
||||
"ruleUnreached": "IMB lostness priority 0 to 2: less than 2% evangelical.",
|
||||
"ruleFrontier": "IMB lostness priority 0: less than 0.1% evangelical. A subset of the unreached.",
|
||||
"ruleDoxa": "Every group on the DOXA prayer list, matched to the IMB data by people and country. Not all are frontier."
|
||||
}
|
||||
13
library/i18n/locales/es/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Todos los grupos étnicos",
|
||||
"locations": "{n} ubicaciones (grupo × país)",
|
||||
"doxa": "Grupos étnicos de DOXA",
|
||||
"frontier": "Frontera — nada iniciado",
|
||||
"unreached": "No alcanzados — el trabajo comienza",
|
||||
"reached": "Alcanzados — la iglesia está ahí",
|
||||
"hint": "Haga clic en una fila para verlos todos · de nuevo para ver todo",
|
||||
"loading": "Cargando todos los grupos étnicos…",
|
||||
"loadError": "No se pudo cargar el mapa.",
|
||||
"pray": "Ore por ellos →",
|
||||
"population": "pobl. {n}"
|
||||
}
|
||||
32
library/i18n/locales/es/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Controles de investigación",
|
||||
"clustering": "Agrupación",
|
||||
"filters": "Filtros",
|
||||
"pin_dim": "Atenuar puntos",
|
||||
"export_poster": "Exportar póster…",
|
||||
"reload_data": "Recargar datos",
|
||||
"off": "Desactivado",
|
||||
"native": "Nativo",
|
||||
"mst": "MST",
|
||||
"network": "Red",
|
||||
"off_desc": "Mostrar cada punto por separado",
|
||||
"native_desc": "Grupos circulares de Mapbox (lo más rápido)",
|
||||
"mst_desc": "Redes de afinidad por árbol de expansión mínima",
|
||||
"network_desc": "Líneas de conexión al vecino más cercano",
|
||||
"search_languages": "Buscar idiomas…",
|
||||
"search_religions": "Buscar religiones…",
|
||||
"reset_all": "Restablecer todo",
|
||||
"no_languages": "Aún no se han cargado idiomas",
|
||||
"no_religions": "Aún no se han cargado religiones",
|
||||
"unadopted": "No adoptado",
|
||||
"min": "mín",
|
||||
"max": "máx",
|
||||
"adopted": "Adoptado",
|
||||
"adoption": "Adopción",
|
||||
"engaged": "Comprometido",
|
||||
"engagement": "Compromiso",
|
||||
"unengaged": "Necesita implicación",
|
||||
"language": "Idioma",
|
||||
"religion": "Religión",
|
||||
"population": "Población"
|
||||
}
|
||||
35
library/i18n/locales/es/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Bloque de afinidad",
|
||||
"gospel_access": "Acceso al evangelio",
|
||||
"people": "Pueblos",
|
||||
"people_name": "Pueblo (nombre)",
|
||||
"praying": "Orando",
|
||||
"religion_family": "Familia religiosa",
|
||||
"search_language": "Buscar idioma",
|
||||
"search_people": "Buscar pueblo",
|
||||
"search_place": "Buscar lugar",
|
||||
"search_religion": "Buscar religión",
|
||||
"upgs_in_scope": "UPG en el ámbito",
|
||||
"countries_touched": "países alcanzados",
|
||||
"unreached": "no alcanzados",
|
||||
"sum_population": "Σ población",
|
||||
"sum_praying": "Σ orando",
|
||||
"charts": "Gráficos",
|
||||
"table": "Tabla",
|
||||
"cluster": "Clúster",
|
||||
"country": "País",
|
||||
"engagement": "Compromiso",
|
||||
"language": "Idioma",
|
||||
"language_family": "Familia lingüística",
|
||||
"people_group": "Pueblo",
|
||||
"population": "Población",
|
||||
"prayer": "Oración",
|
||||
"region": "Región",
|
||||
"religion": "Religión",
|
||||
"status": "Estado",
|
||||
"adopted": "Adoptado",
|
||||
"filters": "Filtros",
|
||||
"gospel_resources": "Recursos del Evangelio",
|
||||
"map": "Mapa",
|
||||
"search_placeholder": "Buscar pueblos, lugares, idiomas, religiones…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "idioma incorrecto",
|
||||
"varietyNotStated": "(variedad no indicada)",
|
||||
"inLanguage": "en"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "Cada punto es UN pueblo de Doxa y su propio idioma; un país es el NÚMERO de esos pueblos.",
|
||||
"noneFoundShort": "«Nada encontrado» significa ausente de las fuentes consultadas, nunca prueba de que no exista.",
|
||||
"tapCountry": "Toca un país para ver los recursos, pueblo por pueblo."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Recursos del evangelio — Datos",
|
||||
"showing": "Mostrando",
|
||||
"of": "de",
|
||||
"for": "para",
|
||||
"filteredTo": "Filtrado por",
|
||||
"showAll": "Mostrar todos los recursos",
|
||||
"t_doxa": "Doxa — Acceso al evangelio",
|
||||
"t_links": "Cada recurso + enlace",
|
||||
"t_nothing": "Donde no se encontró nada",
|
||||
"t_countrygap": "La brecha por país",
|
||||
"t_translation": "Pueden oírlo, sin texto",
|
||||
"t_translationcountry": "Dónde traducir",
|
||||
"t_gaps": "Brechas y oportunidades",
|
||||
"t_bridges": "Puentes urbanos",
|
||||
"t_deaf": "La brecha sorda",
|
||||
"t_resources": "Recursos por pueblo",
|
||||
"t_languages": "Idiomas por país",
|
||||
"t_growth": "Acceso vs crecimiento"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/es/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "Buscar pueblos…",
|
||||
"hide_filters": "Ocultar filtros",
|
||||
"show_filters": "Mostrar filtros",
|
||||
"total": "Total",
|
||||
"showing": "Mostrando",
|
||||
"load_more": "Cargar más",
|
||||
"full_profile": "Perfil completo",
|
||||
"wagf_region": "Región",
|
||||
"wagf_block": "Bloque de afinidad",
|
||||
"rop1": "Pueblo",
|
||||
"religion": "Religión",
|
||||
"exact_filter": "Exacto",
|
||||
"clear_all": "Borrar todo",
|
||||
"loading": "Cargando…",
|
||||
"no_results": "Ningún pueblo coincide con este filtro.",
|
||||
"type_to_search": "Escribe para buscar...",
|
||||
"load_failed": "No se pudieron cargar los datos de pueblos. Es un problema de conexión, no de tu filtro."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menú próximamente",
|
||||
"pinHint": {
|
||||
"tapDot": "Toca un punto de color: alguien todavía necesita oración"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Mi lista UPG 100 — Listas de oración y carteles",
|
||||
"upg100WallTag": "Movilización de oración",
|
||||
"upg100WallIntro": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos <strong>por el nombre de su pueblo</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "País",
|
||||
"upg100Region": "Región",
|
||||
"upg100Zone": "Listas UPG-100",
|
||||
"upg100Group": "Grupo étnico"
|
||||
}
|
||||
"upg100Group": "Grupo étnico",
|
||||
"selectView": "Seleccionar vista",
|
||||
"views": "Vistas",
|
||||
"prayerResources": "Recursos de oración",
|
||||
"map": "Mapa"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/es/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · Visión",
|
||||
"title": "Cada pueblo. Cada lugar.",
|
||||
"tagline": "De todos los grupos étnicos del mundo a aquellos por los que DOXA ora, contados a partir de los datos.",
|
||||
"funnelLabel": "De todos los grupos étnicos a los grupos étnicos de DOXA",
|
||||
"allLabel": "Grupos étnicos",
|
||||
"allNote": "cada grupo étnico, en cada país donde vive",
|
||||
"unreachedLabel": "No alcanzados",
|
||||
"unreachedNote": "menos del 2 % evangélicos",
|
||||
"frontierLabel": "Frontera",
|
||||
"frontierNote": "menos del 0,1 % evangélicos",
|
||||
"doxaLabel": "Grupos étnicos de DOXA",
|
||||
"doxaNote": "{frontier} de ellos son de frontera",
|
||||
"people": "{n} personas",
|
||||
"sources": "Grupos étnicos: {imb}, datos de {date}. Lista de DOXA: pray.doxa.life, {date2}.",
|
||||
"howCounted": "Cómo se cuentan",
|
||||
"ruleAll": "Cada fila de los datos de grupos étnicos del IMB: un grupo étnico en un país.",
|
||||
"ruleUnreached": "Prioridad de perdición del IMB de 0 a 2: menos del 2 % evangélicos.",
|
||||
"ruleFrontier": "Prioridad de perdición del IMB 0: menos del 0,1 % evangélicos. Un subconjunto de los no alcanzados.",
|
||||
"ruleDoxa": "Cada grupo de la lista de oración de DOXA, vinculado a los datos del IMB por pueblo y país. No todos son de frontera."
|
||||
}
|
||||
13
library/i18n/locales/fr/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Tous les groupes ethniques",
|
||||
"locations": "{n} emplacements (groupe × pays)",
|
||||
"doxa": "Groupes ethniques DOXA",
|
||||
"frontier": "Frontière — rien n’a commencé",
|
||||
"unreached": "Non atteints — le travail commence",
|
||||
"reached": "Atteints — l’Église est là",
|
||||
"hint": "Cliquez sur une ligne pour les voir tous · recliquez pour tout voir",
|
||||
"loading": "Chargement de tous les groupes ethniques…",
|
||||
"loadError": "La carte n’a pas pu être chargée.",
|
||||
"pray": "Priez pour eux →",
|
||||
"population": "pop. {n}"
|
||||
}
|
||||
32
library/i18n/locales/fr/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Commandes de recherche",
|
||||
"clustering": "Regroupement",
|
||||
"filters": "Filtres",
|
||||
"pin_dim": "Atténuation des points",
|
||||
"export_poster": "Exporter l’affiche…",
|
||||
"reload_data": "Recharger les données",
|
||||
"off": "Désactivé",
|
||||
"native": "Natif",
|
||||
"mst": "MST",
|
||||
"network": "Réseau",
|
||||
"off_desc": "Afficher chaque point séparément",
|
||||
"native_desc": "Groupes circulaires Mapbox (le plus rapide)",
|
||||
"mst_desc": "Réseaux d’affinité par arbre couvrant minimal",
|
||||
"network_desc": "Lignes vers le plus proche voisin",
|
||||
"search_languages": "Rechercher des langues…",
|
||||
"search_religions": "Rechercher des religions…",
|
||||
"reset_all": "Tout réinitialiser",
|
||||
"no_languages": "Aucune langue chargée pour l’instant",
|
||||
"no_religions": "Aucune religion chargée pour l’instant",
|
||||
"unadopted": "Non adopté",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"adopted": "Adopté",
|
||||
"adoption": "Adoption",
|
||||
"engaged": "Engagé",
|
||||
"engagement": "Engagement",
|
||||
"unengaged": "Besoin d'engagement",
|
||||
"language": "Langue",
|
||||
"religion": "Religion",
|
||||
"population": "Population"
|
||||
}
|
||||
35
library/i18n/locales/fr/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Bloc d’affinité",
|
||||
"gospel_access": "Accès à l’Évangile",
|
||||
"people": "Peuples",
|
||||
"people_name": "Peuple (nom)",
|
||||
"praying": "En prière",
|
||||
"religion_family": "Famille religieuse",
|
||||
"search_language": "Rechercher une langue",
|
||||
"search_people": "Rechercher un peuple",
|
||||
"search_place": "Rechercher un lieu",
|
||||
"search_religion": "Rechercher une religion",
|
||||
"upgs_in_scope": "UPG dans le périmètre",
|
||||
"countries_touched": "pays touchés",
|
||||
"unreached": "non atteints",
|
||||
"sum_population": "Σ population",
|
||||
"sum_praying": "Σ en prière",
|
||||
"charts": "Graphiques",
|
||||
"table": "Tableau",
|
||||
"cluster": "Grappe",
|
||||
"country": "Pays",
|
||||
"engagement": "Engagement",
|
||||
"language": "Langue",
|
||||
"language_family": "Famille linguistique",
|
||||
"people_group": "Peuple",
|
||||
"population": "Population",
|
||||
"prayer": "Prière",
|
||||
"region": "Région",
|
||||
"religion": "Religion",
|
||||
"status": "Statut",
|
||||
"adopted": "Adopté",
|
||||
"filters": "Filtres",
|
||||
"gospel_resources": "Ressources de l'Évangile",
|
||||
"map": "Carte",
|
||||
"search_placeholder": "Rechercher peuples, lieux, langues, religions…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "mauvaise langue",
|
||||
"varietyNotStated": "(variété non précisée)",
|
||||
"inLanguage": "en"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "Un point est UN peuple de Doxa et sa propre langue ; un pays est le NOMBRE de ces peuples.",
|
||||
"noneFoundShort": "« Rien trouvé » signifie absent des sources consultées, jamais la preuve que rien n'existe.",
|
||||
"tapCountry": "Touchez un pays pour voir les ressources, peuple par peuple."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Ressources de l'Évangile — Données",
|
||||
"showing": "Affichage de",
|
||||
"of": "sur",
|
||||
"for": "pour",
|
||||
"filteredTo": "Filtré sur",
|
||||
"showAll": "Afficher toutes les ressources",
|
||||
"t_doxa": "Doxa — Accès à l'Évangile",
|
||||
"t_links": "Chaque ressource + lien",
|
||||
"t_nothing": "Où rien n'a été trouvé",
|
||||
"t_countrygap": "L'écart par pays",
|
||||
"t_translation": "Entendent, sans texte",
|
||||
"t_translationcountry": "Où traduire",
|
||||
"t_gaps": "Écarts et opportunités",
|
||||
"t_bridges": "Ponts urbains",
|
||||
"t_deaf": "L'écart sourd",
|
||||
"t_resources": "Ressources par peuple",
|
||||
"t_languages": "Langues par pays",
|
||||
"t_growth": "Accès vs croissance"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/fr/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "Rechercher des peuples…",
|
||||
"hide_filters": "Masquer les filtres",
|
||||
"show_filters": "Afficher les filtres",
|
||||
"total": "Total",
|
||||
"showing": "Affichage",
|
||||
"load_more": "Charger plus",
|
||||
"full_profile": "Profil complet",
|
||||
"wagf_region": "Région",
|
||||
"wagf_block": "Bloc d'affinité",
|
||||
"rop1": "Peuple",
|
||||
"religion": "Religion",
|
||||
"exact_filter": "Exact",
|
||||
"clear_all": "Tout effacer",
|
||||
"loading": "Chargement…",
|
||||
"no_results": "Aucun peuple ne correspond à ce filtre.",
|
||||
"type_to_search": "Tapez pour rechercher...",
|
||||
"load_failed": "Les données n’ont pas pu être chargées. C’est un problème de connexion, pas votre filtre."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu bientôt disponible",
|
||||
"pinHint": {
|
||||
"tapDot": "Touchez un point coloré — quelqu’un a encore besoin de prière"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "Ma liste UPG 100 — Listes de prière et affiches",
|
||||
"upg100WallTag": "Mobilisation pour la prière",
|
||||
"upg100WallIntro": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux <strong>par le nom de leur peuple</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Pays",
|
||||
"upg100Region": "Région",
|
||||
"upg100Zone": "Listes UPG-100",
|
||||
"upg100Group": "Groupe de peuples"
|
||||
}
|
||||
"upg100Group": "Groupe de peuples",
|
||||
"selectView": "Choisir la vue",
|
||||
"views": "Vues",
|
||||
"prayerResources": "Ressources de prière",
|
||||
"map": "Carte"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/fr/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · Vision",
|
||||
"title": "Chaque peuple. Chaque lieu.",
|
||||
"tagline": "De tous les groupes ethniques du monde à ceux pour qui DOXA prie, comptés à partir des données.",
|
||||
"funnelLabel": "De tous les groupes ethniques aux groupes ethniques DOXA",
|
||||
"allLabel": "Groupes ethniques",
|
||||
"allNote": "chaque groupe ethnique, dans chaque pays où il vit",
|
||||
"unreachedLabel": "Non atteints",
|
||||
"unreachedNote": "moins de 2 % d’évangéliques",
|
||||
"frontierLabel": "Frontière",
|
||||
"frontierNote": "moins de 0,1 % d’évangéliques",
|
||||
"doxaLabel": "Groupes ethniques DOXA",
|
||||
"doxaNote": "dont {frontier} de frontière",
|
||||
"people": "{n} personnes",
|
||||
"sources": "Groupes ethniques : {imb}, données du {date}. Liste DOXA : pray.doxa.life, {date2}.",
|
||||
"howCounted": "Comment ces chiffres sont comptés",
|
||||
"ruleAll": "Chaque ligne des données IMB : un groupe ethnique dans un pays.",
|
||||
"ruleUnreached": "Priorité de perdition IMB 0 à 2 : moins de 2 % d’évangéliques.",
|
||||
"ruleFrontier": "Priorité de perdition IMB 0 : moins de 0,1 % d’évangéliques. Un sous-ensemble des non-atteints.",
|
||||
"ruleDoxa": "Chaque groupe de la liste de prière DOXA, rapproché des données IMB par peuple et pays. Tous ne sont pas de frontière."
|
||||
}
|
||||
13
library/i18n/locales/hi/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "सभी जनसमूह",
|
||||
"locations": "{n} स्थान (समूह × देश)",
|
||||
"doxa": "DOXA जनसमूह",
|
||||
"frontier": "सीमांत — कुछ शुरू नहीं हुआ",
|
||||
"unreached": "अप्रचारित — काम शुरू हो रहा है",
|
||||
"reached": "प्रचारित — कलीसिया मौजूद है",
|
||||
"hint": "सभी देखने के लिए पंक्ति पर क्लिक करें · सब कुछ देखने के लिए फिर क्लिक करें",
|
||||
"loading": "सभी जनसमूह लोड हो रहे हैं…",
|
||||
"loadError": "मानचित्र लोड नहीं हो सका।",
|
||||
"pray": "उनके लिए प्रार्थना करें →",
|
||||
"population": "जनसंख्या {n}"
|
||||
}
|
||||
32
library/i18n/locales/hi/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "शोध नियंत्रण",
|
||||
"clustering": "क्लस्टरिंग",
|
||||
"filters": "फ़िल्टर",
|
||||
"pin_dim": "पिन धुंधलापन",
|
||||
"export_poster": "पोस्टर निर्यात करें…",
|
||||
"reload_data": "डेटा फिर से लोड करें",
|
||||
"off": "बंद",
|
||||
"native": "नेटिव",
|
||||
"mst": "MST",
|
||||
"network": "नेटवर्क",
|
||||
"off_desc": "हर पिन अलग-अलग दिखाएँ",
|
||||
"native_desc": "Mapbox वृत्त क्लस्टर (सबसे तेज़)",
|
||||
"mst_desc": "न्यूनतम स्पैनिंग ट्री आधारित जाल",
|
||||
"network_desc": "निकटतम पड़ोसी को जोड़ने वाली रेखाएँ",
|
||||
"search_languages": "भाषाएँ खोजें…",
|
||||
"search_religions": "धर्म खोजें…",
|
||||
"reset_all": "सब रीसेट करें",
|
||||
"no_languages": "अभी कोई भाषा लोड नहीं हुई",
|
||||
"no_religions": "अभी कोई धर्म लोड नहीं हुआ",
|
||||
"unadopted": "अ-गोद लिया",
|
||||
"min": "न्यून",
|
||||
"max": "अधि",
|
||||
"adopted": "गोद लिया गया",
|
||||
"adoption": "गोद लेना",
|
||||
"engaged": "संपर्कित",
|
||||
"engagement": "सहभागिता",
|
||||
"unengaged": "सहभागिता चाहिए",
|
||||
"language": "भाषा",
|
||||
"religion": "धर्म",
|
||||
"population": "जनसंख्या"
|
||||
}
|
||||
35
library/i18n/locales/hi/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "समानता समूह",
|
||||
"gospel_access": "सुसमाचार पहुँच",
|
||||
"people": "जनसमूह",
|
||||
"people_name": "जनसमूह (नाम)",
|
||||
"praying": "प्रार्थना कर रहे",
|
||||
"religion_family": "धर्म परिवार",
|
||||
"search_language": "भाषा खोजें",
|
||||
"search_people": "जनसमूह खोजें",
|
||||
"search_place": "स्थान खोजें",
|
||||
"search_religion": "धर्म खोजें",
|
||||
"upgs_in_scope": "दायरे में UPG",
|
||||
"countries_touched": "छुए गए देश",
|
||||
"unreached": "अनपहुँचे",
|
||||
"sum_population": "Σ जनसंख्या",
|
||||
"sum_praying": "Σ प्रार्थना",
|
||||
"charts": "चार्ट",
|
||||
"table": "तालिका",
|
||||
"cluster": "क्लस्टर",
|
||||
"country": "देश",
|
||||
"engagement": "सहभागिता",
|
||||
"language": "भाषा",
|
||||
"language_family": "भाषा परिवार",
|
||||
"people_group": "जनसमूह",
|
||||
"population": "जनसंख्या",
|
||||
"prayer": "प्रार्थना",
|
||||
"region": "क्षेत्र",
|
||||
"religion": "धर्म",
|
||||
"status": "स्थिति",
|
||||
"adopted": "गोद लिया गया",
|
||||
"filters": "फ़िल्टर",
|
||||
"gospel_resources": "सुसमाचार संसाधन",
|
||||
"map": "नक्शा",
|
||||
"search_placeholder": "जनसमूह, स्थान, भाषा, धर्म खोजें…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "ग़लत भाषा",
|
||||
"varietyNotStated": "(उपभाषा नहीं बताई गई)",
|
||||
"inLanguage": "में"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "हर बिंदु दोक्सा का एक जनसमूह और उसकी अपनी भाषा है; देश का रंग उन जनसमूहों की संख्या है।",
|
||||
"noneFoundShort": "“कुछ नहीं मिला” का अर्थ है जाँचे गए स्रोतों में नहीं — यह प्रमाण नहीं कि कुछ है ही नहीं।",
|
||||
"tapCountry": "संसाधन देखने के लिए किसी देश पर टैप करें, जनसमूह दर जनसमूह।"
|
||||
},
|
||||
"tables": {
|
||||
"title": "सुसमाचार संसाधन — डेटा",
|
||||
"showing": "दिखा रहे हैं",
|
||||
"of": "में से",
|
||||
"for": "के लिए",
|
||||
"filteredTo": "फ़िल्टर:",
|
||||
"showAll": "सभी संसाधन दिखाएँ",
|
||||
"t_doxa": "दोक्सा — सुसमाचार पहुँच",
|
||||
"t_links": "हर संसाधन + लिंक",
|
||||
"t_nothing": "जहाँ कुछ नहीं मिला",
|
||||
"t_countrygap": "देश अनुसार कमी",
|
||||
"t_translation": "सुन सकते हैं, पाठ नहीं",
|
||||
"t_translationcountry": "अनुवाद कहाँ करें",
|
||||
"t_gaps": "कमियाँ और अवसर",
|
||||
"t_bridges": "नगर सेतु",
|
||||
"t_deaf": "बधिर कमी",
|
||||
"t_resources": "जनसमूह अनुसार संसाधन",
|
||||
"t_languages": "देश अनुसार भाषाएँ",
|
||||
"t_growth": "पहुँच बनाम जनसंख्या वृद्धि"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/hi/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "जनसमूह खोजें…",
|
||||
"hide_filters": "फ़िल्टर छिपाएँ",
|
||||
"show_filters": "फ़िल्टर दिखाएँ",
|
||||
"total": "कुल",
|
||||
"showing": "दिखा रहे हैं",
|
||||
"load_more": "और देखें",
|
||||
"full_profile": "पूरी प्रोफ़ाइल",
|
||||
"wagf_region": "क्षेत्र",
|
||||
"wagf_block": "समानता समूह",
|
||||
"rop1": "जनसमूह",
|
||||
"religion": "धर्म",
|
||||
"exact_filter": "सटीक",
|
||||
"clear_all": "सब हटाएँ",
|
||||
"loading": "लोड हो रहा है…",
|
||||
"no_results": "इस फ़िल्टर से कोई जनसमूह मेल नहीं खाता।",
|
||||
"type_to_search": "खोजने के लिए लिखें...",
|
||||
"load_failed": "जनसमूह डेटा लोड नहीं हो सका। यह कनेक्शन की समस्या है, आपके फ़िल्टर की नहीं।"
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "मेनू शीघ्र आ रहा है",
|
||||
"pinHint": {
|
||||
"tapDot": "किसी रंगीन बिंदु पर टैप करें — किसी को अभी भी प्रार्थना की आवश्यकता है"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
|
||||
"upg100WallTag": "प्रार्थना लामबंदी",
|
||||
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "देश",
|
||||
"upg100Region": "क्षेत्र",
|
||||
"upg100Zone": "UPG-100 सूचियाँ",
|
||||
"upg100Group": "जनसमूह"
|
||||
}
|
||||
"upg100Group": "जनसमूह",
|
||||
"selectView": "दृश्य चुनें",
|
||||
"views": "दृश्य",
|
||||
"prayerResources": "प्रार्थना संसाधन",
|
||||
"map": "मानचित्र"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/hi/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · दर्शन",
|
||||
"title": "हर जनसमूह। हर स्थान।",
|
||||
"tagline": "दुनिया के हर जनसमूह से उन तक जिनके लिए DOXA प्रार्थना करता है, आँकड़ों से गिने गए।",
|
||||
"funnelLabel": "सभी जनसमूहों से DOXA जनसमूहों तक",
|
||||
"allLabel": "जनसमूह",
|
||||
"allNote": "हर जनसमूह, हर उस देश में जहाँ वह रहता है",
|
||||
"unreachedLabel": "अप्रचारित",
|
||||
"unreachedNote": "2% से कम इवेंजेलिकल",
|
||||
"frontierLabel": "सीमांत",
|
||||
"frontierNote": "0.1% से कम इवेंजेलिकल",
|
||||
"doxaLabel": "DOXA जनसमूह",
|
||||
"doxaNote": "इनमें से {frontier} सीमांत हैं",
|
||||
"people": "{n} लोग",
|
||||
"sources": "जनसमूह: {imb}, {date} के आँकड़े। DOXA सूची: pray.doxa.life, {date2}।",
|
||||
"howCounted": "ये कैसे गिने गए",
|
||||
"ruleAll": "IMB जनसमूह आँकड़ों की हर पंक्ति: एक देश में एक जनसमूह।",
|
||||
"ruleUnreached": "IMB लॉस्टनेस प्राथमिकता 0 से 2: 2% से कम इवेंजेलिकल।",
|
||||
"ruleFrontier": "IMB लॉस्टनेस प्राथमिकता 0: 0.1% से कम इवेंजेलिकल। अप्रचारितों का एक भाग।",
|
||||
"ruleDoxa": "DOXA प्रार्थना सूची का हर समूह, जनसमूह और देश के आधार पर IMB आँकड़ों से मिलाया गया। सभी सीमांत नहीं हैं।"
|
||||
}
|
||||
13
library/i18n/locales/it/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Tutti i gruppi etnici",
|
||||
"locations": "{n} località (gruppo × paese)",
|
||||
"doxa": "Gruppi etnici DOXA",
|
||||
"frontier": "Frontiera — non è iniziato nulla",
|
||||
"unreached": "Non raggiunti — il lavoro inizia",
|
||||
"reached": "Raggiunti — la chiesa c’è",
|
||||
"hint": "Fai clic su una riga per vederli tutti · di nuovo per vedere tutto",
|
||||
"loading": "Caricamento di tutti i gruppi etnici…",
|
||||
"loadError": "Impossibile caricare la mappa.",
|
||||
"pray": "Prega per loro →",
|
||||
"population": "pop. {n}"
|
||||
}
|
||||
32
library/i18n/locales/it/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Controlli di ricerca",
|
||||
"clustering": "Raggruppamento",
|
||||
"filters": "Filtri",
|
||||
"pin_dim": "Opacità dei punti",
|
||||
"export_poster": "Esporta poster…",
|
||||
"reload_data": "Ricarica i dati",
|
||||
"off": "Disattivato",
|
||||
"native": "Nativo",
|
||||
"mst": "MST",
|
||||
"network": "Rete",
|
||||
"off_desc": "Mostra ogni punto singolarmente",
|
||||
"native_desc": "Cluster circolari Mapbox (il più veloce)",
|
||||
"mst_desc": "Reti di affinità ad albero ricoprente minimo",
|
||||
"network_desc": "Linee verso il vicino più prossimo",
|
||||
"search_languages": "Cerca lingue…",
|
||||
"search_religions": "Cerca religioni…",
|
||||
"reset_all": "Reimposta tutto",
|
||||
"no_languages": "Nessuna lingua ancora caricata",
|
||||
"no_religions": "Nessuna religione ancora caricata",
|
||||
"unadopted": "Non adottato",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"adopted": "Adottato",
|
||||
"adoption": "Adozione",
|
||||
"engaged": "Coinvolto",
|
||||
"engagement": "Impegno",
|
||||
"unengaged": "Necessita impegno",
|
||||
"language": "Lingua",
|
||||
"religion": "Religione",
|
||||
"population": "Popolazione"
|
||||
}
|
||||
35
library/i18n/locales/it/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Blocco di affinità",
|
||||
"gospel_access": "Accesso al vangelo",
|
||||
"people": "Popoli",
|
||||
"people_name": "Popolo (nome)",
|
||||
"praying": "In preghiera",
|
||||
"religion_family": "Famiglia religiosa",
|
||||
"search_language": "Cerca lingua",
|
||||
"search_people": "Cerca popolo",
|
||||
"search_place": "Cerca luogo",
|
||||
"search_religion": "Cerca religione",
|
||||
"upgs_in_scope": "UPG nell’ambito",
|
||||
"countries_touched": "paesi toccati",
|
||||
"unreached": "non raggiunti",
|
||||
"sum_population": "Σ popolazione",
|
||||
"sum_praying": "Σ in preghiera",
|
||||
"charts": "Grafici",
|
||||
"table": "Tabella",
|
||||
"cluster": "Cluster",
|
||||
"country": "Paese",
|
||||
"engagement": "Impegno",
|
||||
"language": "Lingua",
|
||||
"language_family": "Famiglia linguistica",
|
||||
"people_group": "Popolo",
|
||||
"population": "Popolazione",
|
||||
"prayer": "Preghiera",
|
||||
"region": "Regione",
|
||||
"religion": "Religione",
|
||||
"status": "Stato",
|
||||
"adopted": "Adottato",
|
||||
"filters": "Filtri",
|
||||
"gospel_resources": "Risorse del Vangelo",
|
||||
"map": "Mappa",
|
||||
"search_placeholder": "Cerca popoli, luoghi, lingue, religioni…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "lingua errata",
|
||||
"varietyNotStated": "(varietà non indicata)",
|
||||
"inLanguage": "in"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "Ogni punto è UN popolo di Doxa con la sua lingua; un paese è il NUMERO di quei popoli.",
|
||||
"noneFoundShort": "«Nulla trovato» significa assente dalle fonti controllate, mai la prova che non esista.",
|
||||
"tapCountry": "Tocca un paese per vedere le risorse, popolo per popolo."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Risorse del Vangelo — Dati",
|
||||
"showing": "Mostrando",
|
||||
"of": "di",
|
||||
"for": "per",
|
||||
"filteredTo": "Filtrato per",
|
||||
"showAll": "Mostra tutte le risorse",
|
||||
"t_doxa": "Doxa — Accesso al Vangelo",
|
||||
"t_links": "Ogni risorsa + link",
|
||||
"t_nothing": "Dove non si è trovato nulla",
|
||||
"t_countrygap": "Il divario per paese",
|
||||
"t_translation": "Ascoltano, senza testo",
|
||||
"t_translationcountry": "Dove tradurre",
|
||||
"t_gaps": "Divari e opportunità",
|
||||
"t_bridges": "Ponti urbani",
|
||||
"t_deaf": "Il divario sordo",
|
||||
"t_resources": "Risorse per popolo",
|
||||
"t_languages": "Lingue per paese",
|
||||
"t_growth": "Accesso vs crescita"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
library/i18n/locales/it/grid.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"search": "Cerca popoli…",
|
||||
"hide_filters": "Nascondi filtri",
|
||||
"show_filters": "Mostra filtri",
|
||||
"total": "Totale",
|
||||
"showing": "Mostrati",
|
||||
"load_more": "Carica altro",
|
||||
"full_profile": "Profilo completo",
|
||||
"wagf_region": "Regione",
|
||||
"wagf_block": "Blocco di affinità",
|
||||
"rop1": "Popolo",
|
||||
"religion": "Religione",
|
||||
"exact_filter": "Esatto",
|
||||
"clear_all": "Cancella tutto",
|
||||
"loading": "Caricamento…",
|
||||
"no_results": "Nessun popolo corrisponde a questo filtro.",
|
||||
"type_to_search": "Digita per cercare...",
|
||||
"load_failed": "Impossibile caricare i dati dei popoli. È un problema di connessione, non del filtro."
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
"menuComingSoon": "Menu in arrivo",
|
||||
"pinHint": {
|
||||
"tapDot": "Tocca un punto colorato — qualcuno ha ancora bisogno di preghiera"
|
||||
}
|
||||
},
|
||||
"upg100WallHeading": "La mia lista UPG 100 — Liste di preghiera e poster",
|
||||
"upg100WallTag": "Mobilitazione alla preghiera",
|
||||
"upg100WallIntro": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro <strong>con il nome del loro popolo</strong>."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,9 @@
|
|||
"country": "Paese",
|
||||
"upg100Region": "Regione",
|
||||
"upg100Zone": "Liste UPG-100",
|
||||
"upg100Group": "Gruppo etnico"
|
||||
}
|
||||
"upg100Group": "Gruppo etnico",
|
||||
"selectView": "Seleziona vista",
|
||||
"views": "Viste",
|
||||
"prayerResources": "Risorse di preghiera",
|
||||
"map": "Mappa"
|
||||
}
|
||||
|
|
|
|||
21
library/i18n/locales/it/vision.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kicker": "DOXA · Visione",
|
||||
"title": "Ogni popolo. Ogni luogo.",
|
||||
"tagline": "Da tutti i gruppi etnici del mondo a quelli per cui DOXA prega, contati dai dati.",
|
||||
"funnelLabel": "Da tutti i gruppi etnici ai gruppi etnici DOXA",
|
||||
"allLabel": "Gruppi etnici",
|
||||
"allNote": "ogni gruppo etnico, in ogni paese in cui vive",
|
||||
"unreachedLabel": "Non raggiunti",
|
||||
"unreachedNote": "meno del 2% evangelici",
|
||||
"frontierLabel": "Frontiera",
|
||||
"frontierNote": "meno dello 0,1% evangelici",
|
||||
"doxaLabel": "Gruppi etnici DOXA",
|
||||
"doxaNote": "{frontier} di questi sono di frontiera",
|
||||
"people": "{n} persone",
|
||||
"sources": "Gruppi etnici: {imb}, dati del {date}. Elenco DOXA: pray.doxa.life, {date2}.",
|
||||
"howCounted": "Come vengono contati",
|
||||
"ruleAll": "Ogni riga dei dati IMB sui gruppi etnici: un gruppo etnico in un paese.",
|
||||
"ruleUnreached": "Priorità di perdizione IMB da 0 a 2: meno del 2% evangelici.",
|
||||
"ruleFrontier": "Priorità di perdizione IMB 0: meno dello 0,1% evangelici. Un sottoinsieme dei non raggiunti.",
|
||||
"ruleDoxa": "Ogni gruppo dell’elenco di preghiera DOXA, abbinato ai dati IMB per popolo e paese. Non tutti sono di frontiera."
|
||||
}
|
||||
13
library/i18n/locales/pt/allPeopleGroups.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Todos os grupos étnicos",
|
||||
"locations": "{n} locais (grupo × país)",
|
||||
"doxa": "Grupos étnicos da DOXA",
|
||||
"frontier": "Fronteira — nada iniciado",
|
||||
"unreached": "Não alcançados — o trabalho começa",
|
||||
"reached": "Alcançados — a igreja está lá",
|
||||
"hint": "Clique em uma linha para ver todos · clique de novo para ver tudo",
|
||||
"loading": "Carregando todos os grupos étnicos…",
|
||||
"loadError": "Não foi possível carregar o mapa.",
|
||||
"pray": "Ore por eles →",
|
||||
"population": "pop. {n}"
|
||||
}
|
||||
32
library/i18n/locales/pt/controls.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"research_controls": "Controlos de pesquisa",
|
||||
"clustering": "Agrupamento",
|
||||
"filters": "Filtros",
|
||||
"pin_dim": "Esbater pontos",
|
||||
"export_poster": "Exportar cartaz…",
|
||||
"reload_data": "Recarregar dados",
|
||||
"off": "Desligado",
|
||||
"native": "Nativo",
|
||||
"mst": "MST",
|
||||
"network": "Rede",
|
||||
"off_desc": "Mostrar cada ponto individualmente",
|
||||
"native_desc": "Grupos circulares Mapbox (mais rápido)",
|
||||
"mst_desc": "Redes de afinidade por árvore geradora mínima",
|
||||
"network_desc": "Linhas para o vizinho mais próximo",
|
||||
"search_languages": "Pesquisar idiomas…",
|
||||
"search_religions": "Pesquisar religiões…",
|
||||
"reset_all": "Repor tudo",
|
||||
"no_languages": "Ainda não há idiomas carregados",
|
||||
"no_religions": "Ainda não há religiões carregadas",
|
||||
"unadopted": "Não adotado",
|
||||
"min": "mín",
|
||||
"max": "máx",
|
||||
"adopted": "Adotado",
|
||||
"adoption": "Adoção",
|
||||
"engaged": "Engajado",
|
||||
"engagement": "Engajamento",
|
||||
"unengaged": "Precisa de envolvimento",
|
||||
"language": "Idioma",
|
||||
"religion": "Religião",
|
||||
"population": "População"
|
||||
}
|
||||
35
library/i18n/locales/pt/dashboard.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"bloc": "Bloco de afinidade",
|
||||
"gospel_access": "Acesso ao evangelho",
|
||||
"people": "Povos",
|
||||
"people_name": "Povo (nome)",
|
||||
"praying": "A orar",
|
||||
"religion_family": "Família religiosa",
|
||||
"search_language": "Pesquisar idioma",
|
||||
"search_people": "Pesquisar povo",
|
||||
"search_place": "Pesquisar lugar",
|
||||
"search_religion": "Pesquisar religião",
|
||||
"upgs_in_scope": "UPG no âmbito",
|
||||
"countries_touched": "países alcançados",
|
||||
"unreached": "não alcançados",
|
||||
"sum_population": "Σ população",
|
||||
"sum_praying": "Σ a orar",
|
||||
"charts": "Gráficos",
|
||||
"table": "Tabela",
|
||||
"cluster": "Agrupamento",
|
||||
"country": "País",
|
||||
"engagement": "Engajamento",
|
||||
"language": "Idioma",
|
||||
"language_family": "Família linguística",
|
||||
"people_group": "Povo",
|
||||
"population": "População",
|
||||
"prayer": "Oração",
|
||||
"region": "Região",
|
||||
"religion": "Religião",
|
||||
"status": "Status",
|
||||
"adopted": "Adotado",
|
||||
"filters": "Filtros",
|
||||
"gospel_resources": "Recursos do Evangelho",
|
||||
"map": "Mapa",
|
||||
"search_placeholder": "Pesquisar povos, lugares, idiomas, religiões…"
|
||||
}
|
||||
|
|
@ -98,5 +98,30 @@
|
|||
"wrongLanguage": "idioma incorreto",
|
||||
"varietyNotStated": "(variedade não indicada)",
|
||||
"inLanguage": "em"
|
||||
},
|
||||
"guide": {
|
||||
"dotVsCountry": "Cada ponto é UM povo da Doxa e o seu próprio idioma; um país é o NÚMERO desses povos.",
|
||||
"noneFoundShort": "«Nada encontrado» significa ausente das fontes verificadas, nunca prova de que não exista.",
|
||||
"tapCountry": "Toque num país para ver os recursos, povo a povo."
|
||||
},
|
||||
"tables": {
|
||||
"title": "Recursos do evangelho — Dados",
|
||||
"showing": "Mostrando",
|
||||
"of": "de",
|
||||
"for": "para",
|
||||
"filteredTo": "Filtrado por",
|
||||
"showAll": "Mostrar todos os recursos",
|
||||
"t_doxa": "Doxa — Acesso ao evangelho",
|
||||
"t_links": "Cada recurso + link",
|
||||
"t_nothing": "Onde nada foi encontrado",
|
||||
"t_countrygap": "A lacuna por país",
|
||||
"t_translation": "Ouvem, sem texto",
|
||||
"t_translationcountry": "Onde traduzir",
|
||||
"t_gaps": "Lacunas e oportunidades",
|
||||
"t_bridges": "Pontes urbanas",
|
||||
"t_deaf": "A lacuna surda",
|
||||
"t_resources": "Recursos por povo",
|
||||
"t_languages": "Idiomas por país",
|
||||
"t_growth": "Acesso vs crescimento"
|
||||
}
|
||||
}
|
||||
|
|
|
|||