Merge remote-tracking branch 'origin/development' into cities-plugin/circular-layout
# Conflicts: # package.json
This commit is contained in:
commit
ad63cd56d8
201 changed files with 6142 additions and 1158 deletions
36
README.md
36
README.md
|
|
@ -697,25 +697,29 @@ your own page, set `"locale"` in its `profile-config`. Arabic reads right to lef
|
|||
**Many AI agents can build many plugins at once without breaking each other's work.** Every plugin
|
||||
has one agent that owns it, and the work moves through three kinds of branch:
|
||||
|
||||
- **The pilot branch (`development`)** is the shared working version. A development server with hot
|
||||
reload follows it, so each change that lands can be seen there within about 15 seconds.
|
||||
- **The landing branch (`main`)** is production. It moves only when you publish, and the production
|
||||
server rebuilds from it.
|
||||
- **PR branches** hold one feature each. They start from the landing branch and land on the pilot
|
||||
branch, one at a time, after the checks pass.
|
||||
- **The pilot branch (`development`)** is where reviewed PR branches come together. A pilot server
|
||||
with hot reload follows it, so each change that joins can be seen there within about 15 seconds.
|
||||
- **The landing branch (`main`)** is what ships. It moves only when a person approves a publish, and
|
||||
the landing server rebuilds from it.
|
||||
- **PR branches** hold one plugin or feature each. They start from the landing branch and join the
|
||||
pilot branch, one at a time, after the checks pass and a second reader has read the change.
|
||||
|
||||
**It scales by adding PR branches, not by sharing one.** Many people, each with several agents, can
|
||||
work on many plugins at the same time: every plugin or feature gets its own PR branch, and each one
|
||||
can be seen on the pilot server as soon as it joins the pilot.
|
||||
|
||||
**Bring your own harness.** dx-maps works with whatever AI coding agent or agent harness you already
|
||||
use. The repo gives you the branch strategy below, the checks (`bun run gates:pre`) and the Plugin
|
||||
Studio; it does not ship a harness. Roles such as a coordinator that puts the merges onto the pilot in order, or a
|
||||
gate-keeper that reads each change before it lands, are jobs your harness gives to its agents.
|
||||
|
||||

|
||||

|
||||
|
||||
<sub>[Open the diagram full size](documentation/readme-images/delegation-model.png): on a phone its small text needs it.</sub>
|
||||
|
||||
### Set up the branch strategy
|
||||
|
||||
1. **The landing branch (`main`) is production.** It moves only when you publish (step 7).
|
||||
1. **The landing branch (`main`) is what ships.** It moves only when a person approves a publish (step 7).
|
||||
2. **The pilot branch (`development`) is the shared working branch.** Create it once, from `main`:
|
||||
```bash
|
||||
git fetch origin && git switch -c development origin/main && git push -u origin development
|
||||
|
|
@ -732,7 +736,7 @@ gate-keeper that reads each change before it lands, are jobs your harness gives
|
|||
```bash
|
||||
bun x vite --port 5301 --strictPort # the next agent uses 5302, and so on
|
||||
```
|
||||
5. **Land on the pilot one at a time, through a lock.** Inside the lock, merge your PR branch into a
|
||||
5. **Join the pilot one at a time, through one lock (a merge queue).** Inside the lock, merge your PR branch into a
|
||||
throwaway copy of the pilot branch, run the checks, and push that merge only if nothing fails
|
||||
(`gates:pre` exits non-zero when a check fails, so the push never runs then). Your PR branch itself
|
||||
stays one feature on the landing branch; only the pilot gets the merge:
|
||||
|
|
@ -751,8 +755,8 @@ gate-keeper that reads each change before it lands, are jobs your harness gives
|
|||
BUILD_ALLOW=1 bun run build # the build owner: every plugin
|
||||
BUNDLE=<name> bun x vite build && bun run tree && bun run fix:sri # everyone else: one plugin
|
||||
```
|
||||
7. **Publish:** fast-forward the landing branch to a pilot commit that passed the checks, tag it, and
|
||||
deploy from the tag:
|
||||
7. **Publish, when a person approves it:** fast-forward the landing branch to a pilot commit that
|
||||
passed the checks, tag it, and deploy from the tag:
|
||||
```bash
|
||||
git fetch origin && git switch main && git merge --ff-only origin/main
|
||||
git merge --ff-only <development-commit> # a commit that passed the checks
|
||||
|
|
@ -760,12 +764,14 @@ gate-keeper that reads each change before it lands, are jobs your harness gives
|
|||
git tag -a v1.0.0 -m "Release v1.0.0" && git push origin main v1.0.0
|
||||
```
|
||||
The tag's `public/dx-maps/` folder is the built site: deploy that
|
||||
([how to deploy](./documentation/DEPLOY.md)).
|
||||
([how to deploy](./documentation/DEPLOY.md)). Publish to a public host, such as GitHub, only from
|
||||
the landing branch and only when it is ready.
|
||||
|
||||
### Developing in the cloud with a swarm
|
||||
|
||||
Keep the repository on a forge you host yourself: a swarm pushes often, and on a public host one wrong
|
||||
push publishes work you did not mean to share. Give each app two servers: a **pilot server** that
|
||||
Keep the repository on a git service you host yourself: a swarm pushes often, and an agent can
|
||||
publish by accident. On a public host, one wrong push publishes work you did not mean to share, so
|
||||
publish there only from the landing branch, when it is ready. Give each app two servers: a **pilot server** that
|
||||
follows the pilot branch with hot reload, and a **landing server** that rebuilds from the landing
|
||||
branch. The cloud container in `deployment/cloud/` runs both today (`BRANCH=development` for the pilot server,
|
||||
`PRODUCTION_BRANCH=main` for the landing server; `BRANCH` defaults to `main`). One pilot server per app, rather
|
||||
|
|
@ -798,7 +804,7 @@ So the project works like a busy kitchen:
|
|||
pilot branch only if nothing fails. The pilot branch always works.
|
||||
3. **Someone keeps order.** A coordinator can put the merges onto the pilot in order, and a gate-keeper can read
|
||||
each change by its content before it lands. Your harness gives these jobs to its agents.
|
||||
4. **The landing branch moves only when you publish**, after the checks pass on that exact version.
|
||||
4. **The landing branch moves only when a person approves a publish**, after the checks pass on that exact version.
|
||||
The pilot server follows the pilot branch; the production server rebuilds from the landing branch.
|
||||
|
||||
That's how many plugins get built at the same time, in one project, without the maps breaking.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
"meta": {
|
||||
"title": "Which province each people group falls in — India and China",
|
||||
"why": "Published so no other build has to repeat this point-in-polygon. gospel-internet ran the identical ray-casting over the identical geometry and could only check itself against our aggregates; a second implementation of the same geometry is a second place for it to drift.",
|
||||
"generated": "2026-09-27",
|
||||
"generated": "2026-10-02",
|
||||
"grain": "one row per PLACED people group; the four that fall outside every polygon are in the aggregate file's `unassigned`",
|
||||
"rows": 1009,
|
||||
"agrees_with": "gospel-by-province.json — per-province counts and the assigned total are reconciled at build time, and neither file is written if they differ",
|
||||
"approximate": "Every row is approximate. The coordinate marks where a PEOPLE is found, not a settlement, and near a border it can fall on the wrong side. No surface may present a province as a fact about a group."
|
||||
"approximate": "Every row is approximate. The coordinate marks a PEOPLE group, not a settlement; its position is approximate, and near a border it can fall on the wrong side. No surface may present a province as a fact about a group."
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Gospel access by province — India and China",
|
||||
"why": "These two countries hold 47.9% of Doxa's roster (855 + 154 of 2,107). No other country is close; the third is Sudan at 105. A national fill cannot answer \"where would a team go\" for half the map.",
|
||||
"generated": "2026-09-27",
|
||||
"generated": "2026-10-02",
|
||||
"provinces_with_groups": 46,
|
||||
"provinces_available": 48,
|
||||
"groups_assigned": 1009,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"near_border_note": "NONE unassigned now, and two rows say why. Jing and Pana fell in no province: measured 8.7 km outside Guangxi and 5.4 km outside Yunnan — across the border, not outside the file, which is what an earlier version of this note wrongly said (both provinces are in it). Sourcing China's complete 34-unit ADM1 moved neither distance: same source geometry, 22 more provinces, none of them theirs — so better data is not the answer and nobody need repeat it. A point within 15 km of exactly ONE province now joins it and carries near_border_km; two provinces within the tolerance leave it unassigned, because a hint between two answers is not one. A single coordinate for a whole people is not precise to 9 km, which is why this is a correction and not a guess.",
|
||||
"china_note": "China's ADM1 file carries 12 provinces, not 31+. Measured rather than assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the file is cut to the provinces where the unreached peoples are.",
|
||||
"ways_in": "Chosen FOR THE PROVINCE where the province can answer: the languages of groups standing in it that already have Scripture AND film, ranked by the people here who already hold them. The column says which basis was used. This replaces a country-level answer that produced a measured absurdity — Arunachal Pradesh, in the far north-east, was offered TELUGU, because the curated bridge table is keyed by country and a curated bridge outranks a computed one. A province with no tier-2 language of its own still falls back to the country answer, and ways_in_basis says so rather than hiding it. It is a NEIGHBOUR, not a recommendation: whether a group actually speaks it is a question for someone on the ground.",
|
||||
"approximate": "A province is assigned by point-in-polygon on the group's coordinate, and that coordinate marks where a PEOPLE is found, not a settlement — 108 groups across the roster even share a coordinate with another group. Near a border the point can fall on the wrong side. Every row is flagged approximate and the unassigned are counted, never dropped. No surface may present a province as a fact about a group."
|
||||
"approximate": "A province is assigned by point-in-polygon on the group's coordinate, and that coordinate marks a PEOPLE group, not a settlement; its position is approximate, and 108 groups across the roster even share a coordinate with another group. Near a border the point can fall on the wrong side. Every row is flagged approximate and the unassigned are counted, never dropped. No surface may present a province as a fact about a group."
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Key Cities / Location Clusters",
|
||||
"grain": "one row per ~25km location cluster",
|
||||
"caveat": "A cluster is people-group CENTROIDS, not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"caveat": "A cluster is people-group PINS (each marks a people group, not a town), not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"internet_caveat": "Internet figures are NATIONAL. Per-city connectivity is not publicly available at this scale."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "Languages per City x Resources",
|
||||
"grain": "one row per location cluster x language spoken there",
|
||||
"why": "399 of 1,646 clusters are multi-language — a city-level bridge can exist that a country average hides.",
|
||||
"place_caveat": "A location is a cluster of people-group centroids within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"place_caveat": "A location is a cluster of people-group pins within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"resolution_caveat": "resolved:false means the language LABEL did not match an ISO 639-3 record here — its resource status is UNKNOWN, not absent.",
|
||||
"pools_caveat": "Resource status is from eBible + JESUS Film only. GRN and FCBH audio are key-blocked."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Two things to know before you publish a link:
|
|||
|
||||
- **No zoom or centre parameter.** The frame is derived from the pins the link selects, so a link
|
||||
stays correct when the data moves under it.
|
||||
- **Placement is approximate.** A dot marks where a *people* is found, not a town or a boundary,
|
||||
- **Placement is approximate.** A dot marks a *people group*, not a town or a boundary,
|
||||
and 108 of the placed groups share a coordinate with another group. The sheet says so in the
|
||||
reader's language.
|
||||
- **An unknown parameter is ignored, never fatal.** `?country=ZZZ` renders the map, not an error —
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 767 KiB After Width: | Height: | Size: 926 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="2086" viewBox="0 0 1500 2086" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="2256" viewBox="0 0 1500 2256" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#2563eb"/></marker>
|
||||
<marker id="aS" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#7c3aed"/></marker>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<path id="agL" d="M 194,850 A 556,556 0 0 1 1306,850"/>
|
||||
<path id="mqL" d="M 679,884 A 71,71 0 0 0 821,884"/>
|
||||
</defs>
|
||||
<rect width="1500" height="2086" fill="#ffffff"/>
|
||||
<rect width="1500" height="2256" fill="#ffffff"/>
|
||||
<text x="750" y="54" text-anchor="middle" font-size="34" font-weight="800" fill="#0f172a">How dx-maps is built: one owner per plugin, one merge at a time</text>
|
||||
<circle cx="750" cy="850" r="540" fill="none" stroke="#2563eb" stroke-width="1.6" stroke-dasharray="3 7" opacity=".75"/>
|
||||
<circle cx="750" cy="850" r="355" fill="none" stroke="#15803d" stroke-width="2.2" opacity=".85"/>
|
||||
|
|
@ -173,12 +173,12 @@
|
|||
<text x="1149.0" y="1641" text-anchor="middle" font-size="19" font-weight="800" fill="#fff">4</text>
|
||||
<text x="1177.0" y="1641" font-size="19" font-weight="800" fill="#0f172a">Publish to landing</text>
|
||||
<text x="1135.0" y="1680" font-size="15" fill="#475569">landing (main), production:</text>
|
||||
<text x="1135.0" y="1702" font-size="15" fill="#475569">moves only when the owner approves,</text>
|
||||
<text x="1135.0" y="1702" font-size="15" fill="#475569">moves only when a person approves,</text>
|
||||
<text x="1135.0" y="1724" font-size="15" fill="#475569">after full checks on that version</text>
|
||||
<line x1="750" y1="956" x2="750" y2="1786" stroke="#334155" stroke-width="2.5" stroke-dasharray="2 5" opacity=".0"/>
|
||||
<rect x="60" y="1812" width="1380" height="254" rx="20" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="750" y="1856" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">landing (main) → ONE build → one web folder → three outputs</text>
|
||||
<text x="750" y="1882" text-anchor="middle" font-size="16" fill="#475569">the pilot server follows development · the production server rebuilds from main</text>
|
||||
<text x="750" y="1882" text-anchor="middle" font-size="16" fill="#475569">the pilot server follows development · the landing server rebuilds from main</text>
|
||||
<rect x="80.0" y="1908" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
|
||||
<text x="290.0" y="1930" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 1</text>
|
||||
<text x="290.0" y="1962" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">The web folder</text>
|
||||
|
|
@ -197,4 +197,9 @@
|
|||
<text x="1210.0" y="1988" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Tauri</text>
|
||||
<rect x="1110.0" y="2002" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
|
||||
<text x="1210.0" y="2020" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">WORKING</text>
|
||||
</svg>
|
||||
<rect x="60" y="2090" width="1380" height="146" rx="20" fill="#eff6ff" stroke="#93c5fd" stroke-width="2"/>
|
||||
<text x="750" y="2132" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">At scale: many people × many agents × many plugins</text>
|
||||
<text x="750" y="2164" text-anchor="middle" font-size="16" fill="#475569">one PR branch per plugin or feature → joins the pilot through the lock → previewed on the pilot server</text>
|
||||
<text x="750" y="2192" text-anchor="middle" font-size="16" fill="#475569">in the cloud: a self-hosted git service, because an agent can publish by accident</text>
|
||||
<text x="750" y="2218" text-anchor="middle" font-size="16" fill="#475569">publish to a public host only from landing (main), when it is ready</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -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 29 bundle(s).
|
||||
Generated from 30 bundle(s).
|
||||
|
||||
### One canonical exemplar per tier (generated — quote these)
|
||||
|
||||
|
|
@ -38,6 +38,7 @@ Generated from 29 bundle(s).
|
|||
|---|---|---|
|
||||
| `all-people-groups` | `all-people-groups` | `single` |
|
||||
| `bible-posters` | `bible-posters` | `single` |
|
||||
| `bless-india-posters` | `bless-india-posters` | `single` |
|
||||
| `cities-internet` | `connectivity-list` | `single` |
|
||||
| `cities-internet` | `connectivity-map` | `single` |
|
||||
| `cities-internet` | `data-tables` | `single` |
|
||||
|
|
|
|||
|
|
@ -74,4 +74,4 @@ try {
|
|||
check('Arabic joins the names with و, never an English comma inside Arabic prose', /Koraga وMaila/.test(ar2) && !/Koraga, Maila/.test(ar2), ar2)
|
||||
} finally { await server.close() }
|
||||
if (bad) { console.log(`APPROX-PIN-NOTE: FAIL — ${bad} case(s).`); process.exit(1) }
|
||||
console.log('APPROX-PIN-NOTE: PASS — pins say they are localities, and name the groups that share a point.')
|
||||
console.log('APPROX-PIN-NOTE: PASS — pins say what they are, and name the groups that share a point.')
|
||||
|
|
|
|||
199
internal/check-phone-print-keep.mjs
Normal file
199
internal/check-phone-print-keep.mjs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-phone-print-keep.mjs — on a phone, the in-page print document outlives afterprint, so a saved PDF is the preview.
|
||||
*
|
||||
* THE DEFECT (2026-10-02, on a phone): UPG 100 → Print prayer packet. The print preview showed all 11 pages; "Save as
|
||||
* PDF" saved the WEBSITE (one letter page with the host page's title). On a phone printDocument mounts the document into
|
||||
* the page (#dxm-print-root plus a print-only style) and calls window.print(). It dropped both at afterprint, but
|
||||
* Android Chrome's print() does not block, afterprint fires once the PREVIEW is built, and the saved file is rendered
|
||||
* later, after the reader taps Save, from the page as it is then.
|
||||
*
|
||||
* A timeline on fake timers and a stub DOM, with an Android user agent (Playwright cannot reproduce Android's print
|
||||
* pipeline; a real phone is the real check):
|
||||
* A. after print() and afterprint, the document and its print style are still mounted 30 s on (the Save render);
|
||||
* B. it goes once the reader is BACK: the page shown again after being hidden, the window focused again after
|
||||
* losing focus, or a touch after print(), then BACK_GRACE_MS later, not before;
|
||||
* C. a late print (needs-tap): the Print tap itself does not count as being back, so the document is still
|
||||
* mounted through the dialog;
|
||||
* D. a reader who never comes back: dropped at FRAME_CEILING_MS, not before;
|
||||
* E. printImageBlob: its object URL is NOT revoked at afterprint, and is revoked when the document goes;
|
||||
* F. a new print replaces the previous document (one root mounted, and the previous one released);
|
||||
* G. the desktop path is the frame, as before (printDocument's first line, verbatim);
|
||||
* H. a touch AFTER the first TOUCH_SETTLE_MS, then the print UI opening (the page hidden, or the window blurred):
|
||||
* the pending drop is cancelled, so the document is still mounted well past BACK_GRACE_MS in the UI, and goes
|
||||
* after the next "back" (a measured hole in the first cut, found on a second read);
|
||||
* I. a touch in the first TOUCH_SETTLE_MS after print() (a double tap while the preview builds) is not "back".
|
||||
* `--impl=<path>` runs the timeline against another copy (the pilot's, before this fix, fails A and E: the cause).
|
||||
* `--self-test`: the old drop at afterprint, no release when the reader is back, the listeners armed at mount (so the
|
||||
* Print tap drops it), no grace, printImageBlob revoking at afterprint, no ceiling, no cancel when the page goes away
|
||||
* again during the grace, and no touch settle window must each FAIL.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const implArg = process.argv.find((a) => a.startsWith('--impl='))
|
||||
const SRC = implArg ? implArg.slice(7) : join(ROOT, 'library/composables/printInPlace.js')
|
||||
const ROOT_ID = 'dxm-print-root'
|
||||
|
||||
// ── fake timers ─────────────────────────────────────────────────────────────────────────────────
|
||||
let now = 0, seq = 0
|
||||
const timers = new Map()
|
||||
globalThis.setTimeout = (fn, ms = 0) => { const id = ++seq; timers.set(id, { at: now + Math.max(0, ms), fn }); return id }
|
||||
globalThis.clearTimeout = (id) => { timers.delete(id) }
|
||||
function advance(ms) {
|
||||
const t = now + ms
|
||||
for (;;) {
|
||||
let next = null
|
||||
for (const [id, x] of timers) if (x.at <= t && (!next || x.at < next[1].at || (x.at === next[1].at && id < next[0]))) next = [id, x]
|
||||
if (!next) break
|
||||
timers.delete(next[0]); now = next[1].at; next[1].fn()
|
||||
}
|
||||
now = t
|
||||
}
|
||||
const flush = async () => { for (let i = 0; i < 20; i++) await Promise.resolve() }
|
||||
|
||||
// ── a stub DOM: just what mountPrintRoot touches ────────────────────────────────────────────────────
|
||||
function events() {
|
||||
const L = new Map()
|
||||
return {
|
||||
addEventListener(t, f) { if (!L.has(t)) L.set(t, []); L.get(t).push(f) },
|
||||
removeEventListener(t, f) { const a = L.get(t); if (a && a.indexOf(f) >= 0) a.splice(a.indexOf(f), 1) },
|
||||
fire(t) { for (const f of [...(L.get(t) || [])]) f({ type: t }) },
|
||||
}
|
||||
}
|
||||
function node(tag) {
|
||||
const n = {
|
||||
tag, id: '', attrs: {}, children: [], parent: null, textContent: '', style: {}, ...events(),
|
||||
setAttribute(k, v) { this.attrs[k] = String(v) }, getAttribute(k) { return k in this.attrs ? this.attrs[k] : null },
|
||||
appendChild(c) { c.parent = this; this.children.push(c); return c },
|
||||
append(...cs) { for (const c of cs) this.appendChild(c) },
|
||||
remove() { if (this.parent) { this.parent.children.splice(this.parent.children.indexOf(this), 1); this.parent = null } },
|
||||
querySelectorAll() { return [] },
|
||||
}
|
||||
return n
|
||||
}
|
||||
let revoked = []
|
||||
function world({ android = true, active = true } = {}) {
|
||||
now = 0; seq = 0; timers.clear(); revoked = []
|
||||
const body = node('body'), head = node('head')
|
||||
const doc = {
|
||||
body, head, visibilityState: 'visible', ...events(),
|
||||
createElement: (t) => node(t), importNode: (n) => n,
|
||||
getElementById: (id) => [...body.children, ...head.children].find((n) => n.id === id) || null,
|
||||
querySelectorAll: () => [...body.children.filter((n) => n.id === ROOT_ID), ...head.children.filter((n) => 'data-dxm-print' in n.attrs)],
|
||||
}
|
||||
globalThis.document = doc
|
||||
globalThis.window = { ...events(), prints: [], print() { this.prints.push(mounted()) } }
|
||||
globalThis.DOMParser = class { parseFromString() { return { querySelectorAll: (s) => (s === 'style' ? [{ textContent: '@page{size:letter portrait;margin:0}' }] : []), documentElement: { getAttribute: () => null }, body: { childNodes: [node('div')] } } } }
|
||||
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: {
|
||||
userAgent: android ? 'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Mobile Safari/537.36' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36',
|
||||
userActivation: { isActive: active }, maxTouchPoints: android ? 5 : 0 } })
|
||||
globalThis.URL.createObjectURL = () => 'blob:stub/1'
|
||||
globalThis.URL.revokeObjectURL = (u) => { revoked.push([u, now]) }
|
||||
}
|
||||
const mounted = () => Boolean(document.body.children.find((n) => n.id === ROOT_ID)) && Boolean(document.head.children.find((n) => 'data-dxm-print' in n.attrs))
|
||||
const rootCount = () => document.body.children.filter((n) => n.id === ROOT_ID).length
|
||||
let bust = 0
|
||||
const load = (src) => import('data:text/javascript;base64,' + Buffer.from(src + `\n// ${++bust}`).toString('base64'))
|
||||
|
||||
async function audit(src) {
|
||||
const fails = []
|
||||
const sample = await load(src)
|
||||
const FRAME_CEILING_MS = sample.FRAME_CEILING_MS || 600000, BACK_GRACE_MS = sample.BACK_GRACE_MS || 0, TOUCH_SETTLE_MS = sample.TOUCH_SETTLE_MS || 0
|
||||
if (!sample.FRAME_CEILING_MS || !sample.BACK_GRACE_MS || !sample.TOUCH_SETTLE_MS) fails.push('G: printInPlace.js does not export FRAME_CEILING_MS, BACK_GRACE_MS and TOUCH_SETTLE_MS (the timeline runs on, with 0 for each missing one)')
|
||||
const HTML = '<!doctype html><html><head><style>@page{size:letter portrait}</style></head><body><div>packet</div></body></html>'
|
||||
|
||||
// A + B (hidden → visible)
|
||||
{ world(); const m = await load(src); const how = await m.printDocument(HTML)
|
||||
if (how !== 'page' || window.prints.length !== 1 || !window.prints[0]) fails.push(`A: a phone print did not print the mounted document (returned ${how}, prints ${JSON.stringify(window.prints)})`)
|
||||
window.fire('afterprint'); advance(30000)
|
||||
if (!mounted()) fails.push('A: the print document was gone 30 s after afterprint; a PDF saved then is the bare page')
|
||||
document.visibilityState = 'hidden'; document.fire('visibilitychange'); advance(20000)
|
||||
if (!mounted()) fails.push('B: the document went while the page was hidden (the print dialog was still up)')
|
||||
document.visibilityState = 'visible'; document.fire('visibilitychange'); advance(BACK_GRACE_MS - 1)
|
||||
if (!mounted()) fails.push('B: the document went at once when the page was shown again (no grace for a job still rendering)')
|
||||
advance(2)
|
||||
if (mounted()) fails.push('B: the document stayed after the reader was back on the page (shown again) and the grace')
|
||||
}
|
||||
// B (blur → focus) and B (a touch after print)
|
||||
{ world(); const m = await load(src); await m.printDocument(HTML); window.fire('afterprint'); window.fire('blur'); advance(10000)
|
||||
if (!mounted()) fails.push('B: the document went while the window had lost focus')
|
||||
window.fire('focus'); advance(BACK_GRACE_MS + 1)
|
||||
if (mounted()) fails.push('B: the document stayed after the window was focused again (back on the page)') }
|
||||
{ world(); const m = await load(src); await m.printDocument(HTML); window.fire('afterprint'); advance(TOUCH_SETTLE_MS + 3000); window.fire('pointerdown'); advance(BACK_GRACE_MS + 1)
|
||||
if (mounted()) fails.push('B: the document stayed after the page was touched again') }
|
||||
// C: a late print (the tap's activation expired): the Print tap is not "back"
|
||||
{ world({ active: false }); const m = await load(src); let printNow = null
|
||||
const how = await m.printDocument(HTML, { onNeedsTap: (p) => { printNow = p } })
|
||||
if (how !== 'needs-tap' || !printNow) fails.push(`C: an expired tap did not hand out printNow (returned ${how})`)
|
||||
else {
|
||||
// the reader taps Print a few seconds after the document is ready, then spends longer than the grace in the dialog
|
||||
advance(TOUCH_SETTLE_MS + 3000); window.fire('pointerdown'); printNow(); window.fire('afterprint'); advance(BACK_GRACE_MS + 20000)
|
||||
if (window.prints.length !== 1 || !window.prints[0]) fails.push('C: printNow did not print the mounted document')
|
||||
if (!mounted()) fails.push(`C: the Print tap counted as being back: the document was gone ${(BACK_GRACE_MS + 20000) / 1000} s into the dialog`)
|
||||
} }
|
||||
// D: never back
|
||||
{ world(); const m = await load(src); await m.printDocument(HTML); window.fire('afterprint'); advance(FRAME_CEILING_MS - 1)
|
||||
if (!mounted()) fails.push('D: the document went before the ceiling with the reader never back')
|
||||
advance(2)
|
||||
if (mounted()) fails.push('D: the document outlived FRAME_CEILING_MS') }
|
||||
// E: printImageBlob's object URL
|
||||
{ world(); const m = await load(src); m.printImageBlob({ size: 1 }, { title: 'poster' }); await flush()
|
||||
window.fire('afterprint'); advance(30000)
|
||||
if (revoked.length) fails.push(`E: printImageBlob revoked its object URL at ${revoked[0][1]} ms, before the reader was back (a saved PDF has no image)`)
|
||||
if (!mounted()) fails.push('E: printImageBlob\'s document was gone 30 s after afterprint')
|
||||
window.fire('pointerdown'); advance(BACK_GRACE_MS + 1)
|
||||
if (revoked.length !== 1) fails.push(`E: printImageBlob's object URL was revoked ${revoked.length} time(s) after the reader was back (expected once)`) }
|
||||
// F: a new print replaces the previous one
|
||||
{ world(); const m = await load(src); let first = 0
|
||||
await m.printDocument(HTML, { onRelease: () => { first++ } }); window.fire('afterprint'); await m.printDocument(HTML)
|
||||
if (rootCount() !== 1) fails.push(`F: ${rootCount()} print roots mounted after a second print (expected 1)`)
|
||||
if (first !== 1) fails.push(`F: the first print's onRelease ran ${first} time(s) when a second print replaced it (expected 1)`) }
|
||||
// H: a touch after the settle window, then the print UI opens: away again cancels the pending drop
|
||||
for (const via of ['hidden', 'blur']) {
|
||||
world(); const m = await load(src); await m.printDocument(HTML); advance(TOUCH_SETTLE_MS + 500); window.fire('pointerdown'); advance(500)
|
||||
if (via === 'hidden') { document.visibilityState = 'hidden'; document.fire('visibilitychange') } else window.fire('blur')
|
||||
window.fire('afterprint'); advance(BACK_GRACE_MS + 20000)
|
||||
if (!mounted()) fails.push(`H: a touch, then the print UI (${via}): the grace the touch started was not cancelled, and the document went while the reader was still in the UI (a PDF saved then is the bare page)`)
|
||||
else {
|
||||
if (via === 'hidden') { document.visibilityState = 'visible'; document.fire('visibilitychange') } else window.fire('focus')
|
||||
advance(BACK_GRACE_MS + 1)
|
||||
if (mounted()) fails.push(`H: after the print UI (${via}) closed, the document stayed past the grace (the next "back" did not count)`)
|
||||
}
|
||||
}
|
||||
// I: a touch in the settle window (no hide, no blur: an in-page sheet) is not "back"
|
||||
{ world(); const m = await load(src); await m.printDocument(HTML); advance(Math.max(1, TOUCH_SETTLE_MS - 1500)); window.fire('pointerdown'); advance(BACK_GRACE_MS + 30000)
|
||||
if (!mounted()) fails.push('I: a touch right after print() (a double tap while the preview builds) counted as being back, and the document went with the dialog still up') }
|
||||
// G: desktop unchanged (the frame path)
|
||||
if (!src.includes("if (!isPhonePrint()) return openPrintFrame(html, { autoprint: true }) ? 'frame' : 'blocked'")) fails.push('G: printDocument\'s desktop path is no longer the frame, verbatim')
|
||||
return fails
|
||||
}
|
||||
|
||||
const src = readFileSync(SRC, 'utf8')
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const MOUNT_END = ' ceiling = setTimeout(drop, FRAME_CEILING_MS)\n currentJob = job\n'
|
||||
const poisons = [
|
||||
['the old drop at afterprint', src.replace(MOUNT_END, " window.addEventListener('afterprint', () => setTimeout(drop, 0))\n" + MOUNT_END), /^A: /],
|
||||
['no release when the reader is back', src.replace('const back = () => { if (leaving) return; leaving = true; clearTimeout(ceiling); ceiling = setTimeout(drop, BACK_GRACE_MS) }', 'const back = () => {}'), /^B: the document stayed/],
|
||||
['the listeners armed at mount (the Print tap drops it)', src.replace(MOUNT_END, ' ceiling = setTimeout(drop, FRAME_CEILING_MS)\n job.printing()\n currentJob = job\n'), /^C: the Print tap counted/],
|
||||
['no grace once the reader is back', src.replace('ceiling = setTimeout(drop, BACK_GRACE_MS) }', 'drop() }'), /^B: the document went at once/],
|
||||
['printImageBlob revoking at afterprint', src.replace(" const release = () => { try { URL.revokeObjectURL(url) } catch { /* gone */ } }\n", " const release = () => { try { URL.revokeObjectURL(url) } catch { /* gone */ } }\n window.addEventListener('afterprint', () => setTimeout(release, 0))\n"), /^E: printImageBlob revoked/],
|
||||
['no ceiling', src.replace(MOUNT_END, ' currentJob = job\n'), /^D: the document outlived/],
|
||||
['no cancel when the page goes away again', src.replace('const awayAgain = () => { away = true; if (leaving) { leaving = false; clearTimeout(ceiling); ceiling = setTimeout(drop, FRAME_CEILING_MS) } }', 'const awayAgain = () => { away = true }'), /^H: a touch, then the print UI/],
|
||||
['no touch settle window', src.replace('const onTouch = () => { if (!settling) back() }', 'const onTouch = () => back()'), /^I: /],
|
||||
]
|
||||
let missed = 0
|
||||
for (const [name, poisoned, rx] of poisons) {
|
||||
if (poisoned === src) { console.log(`NOT MEASURED: the poison anchor for "${name}" moved`); process.exitCode = 75; continue }
|
||||
const f = await audit(poisoned); const hit = f.find((x) => rx.test(x))
|
||||
console.log(hit ? `✓ caught: ${name} -> ${hit}` : `✗ MISSED: ${name}${f.length ? ` (failed instead: ${f[0]})` : ''}`)
|
||||
if (!hit) missed++
|
||||
}
|
||||
if (missed) process.exitCode = 1
|
||||
} else {
|
||||
const f = await audit(src)
|
||||
if (f.length) { for (const x of f) console.log('✗ ' + x); console.log(`FAIL check:phone-print-keep (${f.length})`); process.exitCode = 1 }
|
||||
else console.log('PASS check:phone-print-keep — on a phone the print document (and printImageBlob\'s image) outlives afterprint, so a PDF saved after the preview is the document; it goes once the reader is back (shown, focused or touched again) plus a grace, or at the ceiling; the Print tap of a late print and a touch right after print() do not count; going away again cancels a pending drop; a new print replaces it; desktop prints through the frame as before')
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ function assetsBaseFor(depth) {
|
|||
// depth is measured from the WEB-ROOT; the tree root is one level in, hence depth - 1.
|
||||
return DEST_CONFIG.assetsBaseUrl || `${'../'.repeat(Math.max(0, depth - 1))}assets`
|
||||
}
|
||||
// design note 2026-07-11: the staging TOP must not mention deployment orgs (doxa/GO/...). The visible
|
||||
// The staging TOP must not mention deployment orgs (doxa/GO/...). The visible
|
||||
// title is the PRODUCT title (config displayTitle, default org-free), never the deployment TREE_NAME
|
||||
// (TREE_NAME stays in URLs/paths where it is a folder name, not branding).
|
||||
const DISPLAY_TITLE = (typeof DEST_CONFIG.displayTitle === 'string' && DEST_CONFIG.displayTitle.trim())
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* a rect×rect overlap of the sheet against .mapboxgl-ctrl-bottom-left/-right
|
||||
* (their VISIBLE children) in every sheet state.
|
||||
*
|
||||
* WHY: "it feels wrong on my phone" is not evidence a fix can be checked against. This
|
||||
* WHY: a sense that the sheet feels wrong on a phone is not evidence a fix can be checked against. This
|
||||
* probe turns the gesture into a per-step table (finger delta vs measured sheet height),
|
||||
* the release into an expected/actual state, and the attribution into overlap px² —
|
||||
* run it BEFORE a fix (baseline json) and AFTER (same json shape) and diff.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
ApproxPinNote.vue — the ONE wording every map with people-group pins uses (APPROXPIN, 2026-09-26).
|
||||
|
||||
The first sentence always: a dot is where a people is found, not a town or a boundary. The second only
|
||||
The first sentence always: a dot marks a people group, not a town or a boundary. The second only
|
||||
when the pin's point is SHARED, naming the other groups (from library/utils/colocation.js, built from the
|
||||
roster the map draws), because "approximate" alone hides that some dots are the same dot.
|
||||
Never shows the raw coordinate: six decimal places beside a caveat that says "approximate" reads as a
|
||||
|
|
@ -21,8 +21,8 @@ const props = defineProps({
|
|||
// vue-i18n, inside the custom-element boundary, and the reader gets an empty shadow root. Without it
|
||||
// the English below renders, with the same placeholders filled.
|
||||
const APPROX_FALLBACK = {
|
||||
'pins.approximate': 'The dot marks where this people is found, not a town or a boundary.',
|
||||
'pins.sharedPoint': 'It sits on the same point as {names} — {n} groups share this coordinate, so the position tells you the locality and not which group is where.',
|
||||
'pins.approximate': 'The dot marks a people group, not a town or a boundary.',
|
||||
'pins.sharedPoint': 'It sits on the same point as {names} — {n} groups share this dot.',
|
||||
}
|
||||
// C4: the guard is library/composables/useOptionalT.js now. The fallback MAP stays exactly as it was,
|
||||
// passed in, so a bare host renders the same English it rendered before.
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ const packetOpen = ref(false)
|
|||
const { scrimRef, scrimStyle } = useViewportScrim(exportMenuOpen)
|
||||
// Feature flag: show the Export affordance. Gated here in the SHARED legend so
|
||||
// one switch covers every bundle. It was false by an earlier coder decision;
|
||||
// design note 2026-07-31 asked for the button back on the 100-list legend, and the
|
||||
// the button came back on the 100-list legend (2026-07-31), and the
|
||||
// export path is functional (the menu emits 'export' with 'poster' | 'packet',
|
||||
// and a profile wires @export to a real download).
|
||||
// A profile STILL has to opt in with :export-enabled="true" — this flag only
|
||||
|
|
|
|||
|
|
@ -74,20 +74,27 @@ export function openPrintFrame(bodyHtml, { autoprint = false } = {}) {
|
|||
* may ignore, and a cancel that removes the document if the reader walks away. A caller without its own progress UI
|
||||
* passes none and gets the shared Print bar (showTapToPrint).
|
||||
*
|
||||
* ★ ON A PHONE, AFTERPRINT IS NOT THE END OF THE PRINT (2026-10-02). Android Chrome's window.print() does not block,
|
||||
* and afterprint fires once the PREVIEW is built; the file "Save as PDF" writes is rendered later, after the reader
|
||||
* taps Save, from the page as it is THEN. The document used to be dropped at afterprint, so the preview showed a
|
||||
* packet's 11 pages and the saved PDF was the bare host page (one letter page with the page's own title). The
|
||||
* document is now kept until the reader is BACK on the page (mountPrintRoot says how), and onRelease is called then.
|
||||
*
|
||||
* @returns Promise<'frame' | 'page' | 'needs-tap' | 'blocked'>
|
||||
*/
|
||||
export async function printDocument(html, { onNeedsTap = null, imageWaitMs = 20000, labels = null } = {}) {
|
||||
export async function printDocument(html, { onNeedsTap = null, imageWaitMs = 20000, labels = null, onRelease = null } = {}) {
|
||||
if (!isPhonePrint()) return openPrintFrame(html, { autoprint: true }) ? 'frame' : 'blocked'
|
||||
const job = mountPrintRoot(html)
|
||||
const job = mountPrintRoot(html, { onRelease })
|
||||
if (!job) return 'blocked'
|
||||
await job.imagesLoaded(imageWaitMs)
|
||||
const act = typeof navigator !== 'undefined' ? navigator.userActivation : null
|
||||
if (act && act.isActive === false) {
|
||||
const printNow = () => { if (job.live()) window.print() }
|
||||
const printNow = () => { if (job.live()) { job.printing(); window.print() } }
|
||||
if (typeof onNeedsTap === 'function') onNeedsTap(printNow, job.drop)
|
||||
else showTapToPrint(printNow, job.drop, labels || {})
|
||||
return 'needs-tap'
|
||||
}
|
||||
job.printing()
|
||||
window.print()
|
||||
return 'page'
|
||||
}
|
||||
|
|
@ -140,7 +147,7 @@ function showTapToPrint(printNow, cancel, labels = {}) {
|
|||
export const PRINT_ROOT_ID = 'dxm-print-root'
|
||||
let currentJob = null
|
||||
|
||||
function mountPrintRoot(html) {
|
||||
function mountPrintRoot(html, { onRelease = null } = {}) {
|
||||
if (typeof document === 'undefined' || !document.body || typeof DOMParser === 'undefined') return null
|
||||
if (currentJob) currentJob.drop()
|
||||
// EVERY BUNDLE CARRIES ITS OWN COPY OF THIS FILE, so currentJob only knows this bundle's root: a root (and its
|
||||
|
|
@ -169,11 +176,35 @@ function mountPrintRoot(html) {
|
|||
document.head.appendChild(style)
|
||||
document.body.appendChild(root)
|
||||
|
||||
let ceiling = null
|
||||
const onAfter = () => setTimeout(drop, 0)
|
||||
// ★ KEPT UNTIL THE READER IS BACK, never dropped at afterprint (printDocument says why). From the moment print() is
|
||||
// called (printing(), not at mount: the Print tap that starts a late print must not count), the reader is back when
|
||||
// the page is shown again after being hidden, the window is focused again after losing focus, or the page is
|
||||
// touched (the system print UI covers the page, so a touch there means it has gone). A touch in the first
|
||||
// TOUCH_SETTLE_MS after print() does not count (a double tap while the preview builds). It goes BACK_GRACE_MS after
|
||||
// that, since a slow phone can still be writing the file after its dialog closes; if the page goes AWAY again during
|
||||
// the grace (the print UI opened after an early touch), the pending drop is cancelled and the next "back" counts. A
|
||||
// new print replaces it, a cancelled one drops it, and FRAME_CEILING_MS is the ceiling. On screen it is display:none,
|
||||
// so keeping it shows nothing.
|
||||
let ceiling = null, settle = null, armed = false, away = false, leaving = false, settling = false
|
||||
const back = () => { if (leaving) return; leaving = true; clearTimeout(ceiling); ceiling = setTimeout(drop, BACK_GRACE_MS) }
|
||||
const awayAgain = () => { away = true; if (leaving) { leaving = false; clearTimeout(ceiling); ceiling = setTimeout(drop, FRAME_CEILING_MS) } }
|
||||
const onVisibility = () => { if (document.visibilityState === 'hidden') awayAgain(); else if (away) back() }
|
||||
const onBlur = () => { awayAgain() }
|
||||
const onFocus = () => { if (away) back() }
|
||||
const onTouch = () => { if (!settling) back() }
|
||||
const job = {
|
||||
live: () => currentJob === job,
|
||||
drop,
|
||||
printing: () => {
|
||||
if (armed) return
|
||||
armed = true
|
||||
settling = true
|
||||
settle = setTimeout(() => { settling = false }, TOUCH_SETTLE_MS)
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
window.addEventListener('blur', onBlur)
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('pointerdown', onTouch, true)
|
||||
},
|
||||
imagesLoaded: (ms) => {
|
||||
const pending = [...root.querySelectorAll('img')].filter((img) => !img.complete)
|
||||
if (!pending.length) return Promise.resolve()
|
||||
|
|
@ -186,12 +217,16 @@ function mountPrintRoot(html) {
|
|||
}
|
||||
function drop() {
|
||||
clearTimeout(ceiling)
|
||||
window.removeEventListener('afterprint', onAfter)
|
||||
clearTimeout(settle)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
window.removeEventListener('focus', onFocus)
|
||||
window.removeEventListener('pointerdown', onTouch, true)
|
||||
root.remove()
|
||||
style.remove()
|
||||
if (currentJob === job) currentJob = null
|
||||
if (onRelease) { const done = onRelease; onRelease = null; try { done() } catch { /* the caller's own clean-up */ } }
|
||||
}
|
||||
window.addEventListener('afterprint', onAfter)
|
||||
ceiling = setTimeout(drop, FRAME_CEILING_MS)
|
||||
currentJob = job
|
||||
return job
|
||||
|
|
@ -201,6 +236,10 @@ function mountPrintRoot(html) {
|
|||
export const FRAME_KEEP_MS = 60000
|
||||
/** The absolute ceiling for a print frame whose load never ends. */
|
||||
export const FRAME_CEILING_MS = 600000
|
||||
/** A phone's in-page print document goes this long after the reader is back on the page (mountPrintRoot). */
|
||||
export const BACK_GRACE_MS = 60000
|
||||
/** A touch this soon after print() is not the reader coming back (a double tap while the preview builds). */
|
||||
export const TOUCH_SETTLE_MS = 2000
|
||||
|
||||
/**
|
||||
* Print a rendered poster IMAGE, at its real paper size, through the same in-place frame.
|
||||
|
|
@ -234,14 +273,11 @@ export function printImageBlob(blob, { widthIn = 8.5, heightIn = 11, title = '',
|
|||
+ 'html,body{margin:0;padding:0;background:#fff}'
|
||||
+ 'img{display:block;width:100%;height:100%;object-fit:contain}</style></head>'
|
||||
+ `<body><img src="${url}" alt="${esc(alt || title)}"></body></html>`
|
||||
// A PHONE PRINTS THE PAGE (printDocument). The object URL lives until the page has printed, or the ceiling.
|
||||
// A PHONE PRINTS THE PAGE (printDocument). The object URL lives as long as the in-page document does: until the
|
||||
// reader is back on the page, a new print or the ceiling, NEVER afterprint (a saved PDF is rendered after it).
|
||||
if (isPhonePrint()) {
|
||||
let clock = null
|
||||
const onAfter = () => setTimeout(release, 0)
|
||||
const release = () => { clearTimeout(clock); window.removeEventListener('afterprint', onAfter); try { URL.revokeObjectURL(url) } catch { /* gone */ } }
|
||||
window.addEventListener('afterprint', onAfter)
|
||||
clock = setTimeout(release, FRAME_CEILING_MS)
|
||||
printDocument(html, { onNeedsTap, labels }).then((how) => { if (how === 'blocked') release() }, release)
|
||||
const release = () => { try { URL.revokeObjectURL(url) } catch { /* gone */ } }
|
||||
printDocument(html, { onNeedsTap, labels, onRelease: release }).then((how) => { if (how === 'blocked') release() }, release)
|
||||
return true
|
||||
}
|
||||
const win = openPrintFrame(html, { autoprint: true })
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
* either edge can be summoned where you reach for it. A HOVERED strip is
|
||||
* pinned: it never auto-hides (even while you scroll) until you leave it.
|
||||
*
|
||||
* RIGHT is the primary side on EVERY form factor (design note, 2026-07-30): people
|
||||
* RIGHT is the primary side on EVERY form factor (since 2026-07-30): people
|
||||
* expect a scrollbar on the right, and a scroll hint that moves between edges
|
||||
* depending on device is the one thing it must not do. An earlier version flipped
|
||||
* to LEFT on mobile for thumb reach; consistency won. Both strips stay summonable
|
||||
|
|
@ -71,7 +71,7 @@ export function useEdgeScrollAffordance(wrapper, opts = {}) {
|
|||
const show = (e) => { e.style.opacity = '1' }
|
||||
const hide = (e) => { e.style.opacity = '0' }
|
||||
|
||||
// Primary side: RIGHT on BOTH desktop and mobile (design note, 2026-07-30).
|
||||
// Primary side: RIGHT on BOTH desktop and mobile (since 2026-07-30).
|
||||
// It used to flip to LEFT on mobile on a thumb-reach argument, but that made the
|
||||
// affordance appear on a different edge depending on device — the one place a
|
||||
// scroll hint must be predictable. People expect a scrollbar on the right, and
|
||||
|
|
|
|||
15
library/i18n/locales/ar/blessIndiaPosters.json
Normal file
15
library/i18n/locales/ar/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "ملصقات باركوا الهند",
|
||||
"rangeBadge60x96": "شارات النطاق · 60×96 (5×8 قدم)",
|
||||
"count": "{count} مجموعة شعبية",
|
||||
"desktopOnly": "سطح المكتب فقط حاليًا — لم يَثبُت بعد أن هذا الملصق الكبير يُطبع بأمان على الهاتف.",
|
||||
"experimental": "تجريبي",
|
||||
"intro": "كل مجموعة شعبية في الهند على ورقة واحدة قابلة للطباعة، تُولَّد مباشرةً وتُعرَض كأوراقها القابلة للطباعة مع شرائط اللصق — بالطريقة نفسها التي يعرض بها عرض المقاسات التنسيقات الكبيرة.",
|
||||
"print": "اطبع هذا الملصق",
|
||||
"attempt": "شارات النطاق · 60×96 (5×8 قدم) · 3 أوراق",
|
||||
"stat": "{groups} مجموعة · {badges} شارة نطاق · 0 تداخل",
|
||||
"generating": "جارٍ توليد الملصق من البيانات الحية…",
|
||||
"error": "تعذَّر توليد الملصق: {msg}",
|
||||
"photos": "فقط {real} من {total} مجموعة في الهند لديها صورة في البيانات؛ المستند المطبوع يحملها، بينما الصورة المسطحة لا تستطيع. هذا التصميم المرقَّم يطبع الأسماء بدون صور لتكون كل مجموعة متساوية وواضحة.",
|
||||
"posterHeader": "باركوا الهند باسم يسوع"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "تطبيق المراسلة الأول (البلد)",
|
||||
"messagingNote": "الأكثر تنزيلًا في متجر App Store الخاص بالبلد: iOS فقط، ترتيب تنزيلات لا استخدام.",
|
||||
"province": "الهند والصين: الولاية أو المقاطعة",
|
||||
"provinceNote": "تقريبي: بحسب مكان وجود الشعب، لا بحسب مدينة.",
|
||||
"provinceNote": "تقريبي: مأخوذ من نقطة المجموعة على الخريطة، لا من مدينة.",
|
||||
"region": "المنطقة",
|
||||
"religion": "الديانة",
|
||||
"wayIn": "المدخل حين لا يوجد شيء بلغتهم"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "خريطة المجموعات العرقية، مظلّلة بحسب نسبة استخدام الإنترنت في كل بلد"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "تشير النقطة إلى مكان وجود هذا الشعب في {country}، لا إلى مدينة.",
|
||||
"pins": "تشير النقطة إلى مكان وجود الشعب في بلده، لا إلى مدينة. {n} مجموعة تشترك في نقطة مع مجموعة أخرى.",
|
||||
"pin": "تمثّل النقطة هذا الشعب، لا مدينة.",
|
||||
"pins": "تمثّل النقطة شعبًا، لا مدينة. {n} مجموعة تشترك في نقطة مع مجموعة أخرى.",
|
||||
"shares": "تشترك في نقطتها مع {n} مجموعة أخرى.",
|
||||
"sources": "المصادر:",
|
||||
"standing": "النقاط تقريبية (مكان وجود الشعب في بلده، لا مدينة). أرقام الإنترنت هي أرقام البلد أو الولاية الهندية، لا أرقام المجموعة أبدًا. «وُجد» تعني وُجد في المصادر التي فُحصت."
|
||||
"standing": "النقاط تقريبية: النقطة تمثّل شعبًا، لا مدينة. أرقام الإنترنت هي أرقام البلد أو الولاية الهندية، لا أرقام المجموعة أبدًا. «وُجد» تعني وُجد في المصادر التي فُحصت."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "رسوم بيانية",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "حيث تعيش الشعوب غير المُبشَّرة",
|
||||
"title": "عناقيد نقاط الشعوب",
|
||||
"bands": {
|
||||
"named": "مركز مُسمّى",
|
||||
"multi": "متعدد الشعوب",
|
||||
"single": "شعب واحد"
|
||||
},
|
||||
"note": "النقطة عنقود من مراكز الشعوب ضمن نحو 25 كم، وليست حدودًا بلدية. النقاط المُسمّاة مأخوذة من معجم جغرافي موثّق.",
|
||||
"note": "النقطة عنقود من نقاط الشعوب ضمن نحو 25 كم، وليست حدودًا بلدية. النقاط المُسمّاة مأخوذة من معجم جغرافي موثّق.",
|
||||
"unnamed": "موقع بلا اسم — {lat}، {lng}",
|
||||
"close": "إغلاق",
|
||||
"country": "الدولة",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "غير معروف",
|
||||
"pgHere": "الشعوب هنا",
|
||||
"languages": "اللغات",
|
||||
"sheetNote": "عنقود من مراكز الشعوب ضمن نحو 25 كم، وليس حدودًا بلدية. النقاط المُسمّاة مأخوذة من معجم جغرافي موثّق (Nominatim/OSM)؛ والنقاط بلا اسم تُظهر الإحداثيات بدل اسم مختلَق.",
|
||||
"sheetNote": "عنقود من نقاط الشعوب ضمن نحو 25 كم، وليس حدودًا بلدية. النقاط المُسمّاة مأخوذة من معجم جغرافي موثّق (Nominatim/OSM)؛ والنقاط بلا اسم تُظهر الإحداثيات بدل اسم مختلَق.",
|
||||
"national": "أرقام الإنترنت وطنية.",
|
||||
"engineMissing": "تعذّر تحميل محرك الخريطة."
|
||||
},
|
||||
"list": {
|
||||
"title": "المدن الرئيسية",
|
||||
"intro": "حيث تعيش الشعوب غير المُبشَّرة: مراكز سكانية مُسمّاة من معجم جغرافي موثّق: {named}، ضمن عناقيد محددة الموقع: {clusters}.",
|
||||
"intro": "عناقيد نقاط الشعوب: عناقيد مُسمّاة (الأسماء من معجم جغرافي موثّق): {named}، ضمن عناقيد محددة الموقع: {clusters}.",
|
||||
"filters": {
|
||||
"named": "المراكز السكانية المُسمّاة",
|
||||
"multi": "عناقيد متعددة الشعوب",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "الشعوب: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "ما الذي يمثّله الصف.",
|
||||
"row": "عنقود من مراكز الشعوب ضمن نحو 25 كم، وليس حدودًا بلدية. تدل النقاط على مكان تمركز الشعب، لا على مستوطنة، لذلك لا تُسمّى العناقيد بالاستنتاج أبدًا: حُدِّدت المراكز متعددة الشعوب وعددها {n} عبر {source} (ODbL، مع ذكر المصدر في كل صف)، وتبقى البقية بلا اسم بدل التخمين.",
|
||||
"row": "عنقود من نقاط الشعوب ضمن نحو 25 كم، وليس حدودًا بلدية. تمثّل كل نقطة شعبًا، لا بلدة، لذلك لا تُسمّى العناقيد بالاستنتاج أبدًا: حُدِّدت المراكز متعددة الشعوب وعددها {n} عبر {source} (ODbL، مع ذكر المصدر في كل صف)، وتبقى البقية بلا اسم بدل التخمين.",
|
||||
"internet": "تُعرض كسياق: بيانات الاتصال لكل مدينة غير متاحة للعموم بهذا المستوى، ولا يجوز قراءة الرقم الوطني على أنه قياس للمدينة.",
|
||||
"scope": "تحمل هذه الصفحة مواقع الشعوب وبيانات الاتصال فقط — لا بيانات عن كنائس أو مؤمنين أو أماكن اجتماع، وذلك عن قصد."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "تشير النقطة إلى المكان الذي يوجد فيه هذا الشعب، وليست بلدة أو حدودًا.",
|
||||
"sharedPoint": "تقع في النقطة نفسها مع {names} — {n} مجموعات تتشارك هذا الإحداثي، لذا يدل الموقع على المنطقة لا على مكان كل مجموعة."
|
||||
"approximate": "تمثّل النقطة شعبًا، لا بلدة ولا حدودًا.",
|
||||
"sharedPoint": "تقع في النقطة نفسها مع {names} — {n} مجموعات تتشارك هذه النقطة."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/de/blessIndiaPosters.json
Normal file
15
library/i18n/locales/de/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "„Segne Indien“-Poster",
|
||||
"rangeBadge60x96": "Nummern-Marken · 60×96 (5×8 Fuß)",
|
||||
"count": "{count} Volksgruppen",
|
||||
"desktopOnly": "Vorerst nur am Desktop — es ist noch nicht erwiesen, dass dieses große Poster sicher auf einem Telefon druckt.",
|
||||
"experimental": "experimentell",
|
||||
"intro": "Jede Volksgruppe Indiens auf einem druckbaren Bogen, live erzeugt und als ihre druckbaren Bögen mit Klebestreifen gezeigt — so wie die Größen-Ansicht von Poster Design die großen Formate zeigt.",
|
||||
"print": "Dieses Poster drucken",
|
||||
"attempt": "Nummern-Marken · 60×96 (5×8 Fuß) · 3 Bögen",
|
||||
"stat": "{groups} Gruppen · {badges} Nummern-Marken · 0 Überlappungen",
|
||||
"generating": "Poster wird aus den Live-Daten erzeugt…",
|
||||
"error": "Poster konnte nicht erzeugt werden: {msg}",
|
||||
"photos": "Nur {real} von {total} indischen Gruppen haben ein Foto in den Daten; ein gedrucktes Dokument trägt sie, ein flaches Bild nicht. Dieses nummerierte Design druckt Namen ohne Fotos, damit jede Gruppe gleich und lesbar ist.",
|
||||
"posterHeader": "Segne Indien im Namen Jesu"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "Top-Messenger (Land)",
|
||||
"messagingNote": "Am meisten heruntergeladen im Apple App Store des Landes: nur iOS, ein Download-Rang, keine Nutzung.",
|
||||
"province": "Indien und China: Bundesstaat oder Provinz",
|
||||
"provinceNote": "Ungefähr: danach zugeordnet, wo ein Volk lebt, nicht nach einem Ort.",
|
||||
"provinceNote": "Ungefähr: aus dem Punkt der Gruppe auf der Karte abgeleitet, nicht aus einem Ort.",
|
||||
"region": "Region",
|
||||
"religion": "Religion",
|
||||
"wayIn": "Zugang, wenn nichts in ihrer Sprache vorliegt"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Karte der Volksgruppen, schattiert danach, wie viele Menschen im Land das Internet nutzen"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "Der Punkt markiert, wo dieses Volk in {country} lebt, nicht einen Ort.",
|
||||
"pins": "Ein Punkt markiert, wo ein Volk in seinem Land lebt, nicht einen Ort. {n} Gruppen teilen sich einen Punkt mit einer anderen Gruppe.",
|
||||
"pin": "Der Punkt steht für dieses Volk, nicht für einen Ort.",
|
||||
"pins": "Ein Punkt steht für ein Volk, nicht für einen Ort. {n} Gruppen teilen sich einen Punkt mit einer anderen Gruppe.",
|
||||
"shares": "Er teilt seinen Punkt mit {n} anderen Gruppe(n).",
|
||||
"sources": "Quellen:",
|
||||
"standing": "Punkte sind ungefähr (wo ein Volk in seinem Land lebt, nicht ein Ort). Internetzahlen gelten für das Land oder den indischen Bundesstaat, nie für die Gruppe. „Gefunden“ heißt: in den geprüften Quellen gefunden."
|
||||
"standing": "Punkte sind ungefähr: Ein Punkt steht für ein Volk, nicht für einen Ort. Internetzahlen gelten für das Land oder den indischen Bundesstaat, nie für die Gruppe. „Gefunden“ heißt: in den geprüften Quellen gefunden."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Diagramme",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Wo die unerreichten Volksgruppen leben",
|
||||
"title": "Cluster von Volksgruppen-Punkten",
|
||||
"bands": {
|
||||
"named": "benanntes Zentrum",
|
||||
"multi": "mehrere Gruppen",
|
||||
"single": "eine Gruppe"
|
||||
},
|
||||
"note": "Ein Punkt ist ein Cluster von Volksgruppen-Mittelpunkten im Umkreis von ~25 km, keine Gemeindegrenze. Benannte Punkte stammen aus einem belegten Ortsverzeichnis.",
|
||||
"note": "Ein Punkt ist ein Cluster von Volksgruppen-Punkten im Umkreis von ~25 km, keine Gemeindegrenze. Benannte Punkte stammen aus einem belegten Ortsverzeichnis.",
|
||||
"unnamed": "unbenannter Ort — {lat}, {lng}",
|
||||
"close": "Schließen",
|
||||
"country": "Land",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "unbekannt",
|
||||
"pgHere": "Volksgruppen hier",
|
||||
"languages": "Sprachen",
|
||||
"sheetNote": "Ein Cluster von Volksgruppen-Mittelpunkten im Umkreis von ~25 km, keine Gemeindegrenze. Benannte Punkte stammen aus einem belegten Ortsverzeichnis (Nominatim/OSM); unbenannte zeigen Koordinaten statt eines erfundenen Ortsnamens.",
|
||||
"sheetNote": "Ein Cluster von Volksgruppen-Punkten im Umkreis von ~25 km, keine Gemeindegrenze. Benannte Punkte stammen aus einem belegten Ortsverzeichnis (Nominatim/OSM); unbenannte zeigen Koordinaten statt eines erfundenen Ortsnamens.",
|
||||
"national": "Internetzahlen sind NATIONAL.",
|
||||
"engineMissing": "Die Karten-Engine konnte nicht geladen werden."
|
||||
},
|
||||
"list": {
|
||||
"title": "Schlüsselstädte",
|
||||
"intro": "Wo die unerreichten Volksgruppen leben: benannte Bevölkerungszentren aus einem belegten Ortsverzeichnis: {named}, in lokalisierten Clustern: {clusters}.",
|
||||
"intro": "Cluster von Volksgruppen-Punkten: benannte Cluster (Namen aus einem belegten Ortsverzeichnis): {named}, in lokalisierten Clustern: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Benannte Bevölkerungszentren",
|
||||
"multi": "Cluster mit mehreren Gruppen",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "Volksgruppen: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Was eine Zeile ist.",
|
||||
"row": "Ein Cluster von Volksgruppen-Mittelpunkten im Umkreis von ~25 km, keine Gemeindegrenze. Die Punkte markieren, wo eine Volksgruppe ihren Mittelpunkt hat, nicht eine Siedlung, daher werden Cluster nie durch Schlussfolgerung benannt: Die {n} Zentren mit mehreren Volksgruppen wurden über {source} bestimmt (ODbL, pro Zeile zitiert), der Rest bleibt unbenannt, statt geraten zu werden.",
|
||||
"row": "Ein Cluster von Volksgruppen-Punkten im Umkreis von ~25 km, keine Gemeindegrenze. Jeder Punkt steht für eine Volksgruppe, nicht für einen Ort, daher werden Cluster nie durch Schlussfolgerung benannt: Die {n} Zentren mit mehreren Volksgruppen wurden über {source} bestimmt (ODbL, pro Zeile zitiert), der Rest bleibt unbenannt, statt geraten zu werden.",
|
||||
"internet": "Sie dienen als Kontext: Konnektivität pro Stadt ist in diesem Maßstab nicht öffentlich verfügbar, und eine nationale Zahl darf nicht als Messung für eine Stadt gelesen werden.",
|
||||
"scope": "Diese Seite enthält nur Standorte von Volksgruppen und Konnektivität — bewusst keine Daten zu Gemeinden, Gläubigen oder Treffpunkten."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "Der Punkt zeigt, wo dieses Volk lebt – nicht eine Stadt oder eine Grenze.",
|
||||
"sharedPoint": "Er liegt auf demselben Punkt wie {names} — {n} Gruppen teilen sich diese Koordinate; die Position nennt also die Gegend, nicht wo welche Gruppe lebt."
|
||||
"approximate": "Der Punkt steht für ein Volk, nicht für eine Stadt oder eine Grenze.",
|
||||
"sharedPoint": "Er liegt auf demselben Punkt wie {names} — {n} Gruppen teilen sich diesen Punkt."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/en/blessIndiaPosters.json
Normal file
15
library/i18n/locales/en/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Bless India Posters",
|
||||
"rangeBadge60x96": "Range badges · 60×96 (5×8 ft)",
|
||||
"count": "{count} people groups",
|
||||
"desktopOnly": "Desktop only for now — this large poster isn’t yet proven to print safely on a phone.",
|
||||
"experimental": "experimental",
|
||||
"intro": "Every India people group on one printable sheet, generated live and shown as its printable sheets with glue strips — the way Poster Design's Sizes view shows the big formats.",
|
||||
"print": "Print this poster",
|
||||
"attempt": "Range badges · 60×96 (5×8 ft) · 3 sheets",
|
||||
"stat": "{groups} groups · {badges} range badges · 0 overlaps",
|
||||
"generating": "Generating the poster from the live data…",
|
||||
"error": "Couldn't generate the poster: {msg}",
|
||||
"photos": "Only {real} of {total} India groups have a photo in the data; a printed document carries them, a flat image can't. This numbered design prints names without photos so every group is equal and legible.",
|
||||
"posterHeader": "Bless India in Jesus' name"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "Top messaging app (country)",
|
||||
"messagingNote": "Most downloaded in the country's Apple App Store: iOS only, a download rank, not use.",
|
||||
"province": "India and China: state or province",
|
||||
"provinceNote": "Approximate: placed from where a people is found, not a town.",
|
||||
"provinceNote": "Approximate: taken from the group's dot on the map, not from a town.",
|
||||
"region": "Region",
|
||||
"religion": "Religion",
|
||||
"wayIn": "Way in when nothing is in their language"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Map of people groups, shaded by how much of each country uses the internet"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "The dot marks where this people is found in {country}, not a town.",
|
||||
"pins": "A dot marks where a people is found in its country, not a town. {n} groups share a point with another group.",
|
||||
"pin": "The dot marks this people group, not a town.",
|
||||
"pins": "A dot marks a people group, not a town. {n} groups share a point with another group.",
|
||||
"shares": "It shares its point with {n} other group(s).",
|
||||
"sources": "Sources:",
|
||||
"standing": "Dots are approximate (where a people is found in its country, not a town). Internet figures are the country's, or the Indian state's, never the group's. \"Found\" means found in the sources checked."
|
||||
"standing": "Dots are approximate: a dot marks a people group, not a town. Internet figures are the country's, or the Indian state's, never the group's. \"Found\" means found in the sources checked."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Charts",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Where the unengaged people groups live",
|
||||
"title": "Clusters of people-group pins",
|
||||
"bands": {
|
||||
"named": "named centre",
|
||||
"multi": "multi-group",
|
||||
"single": "single group"
|
||||
},
|
||||
"note": "A point is a cluster of people-group centroids within ~25 km, not a municipal boundary. Named points come from a cited gazetteer.",
|
||||
"note": "A point is a cluster of people-group pins within ~25 km, not a municipal boundary. Named points come from a cited gazetteer.",
|
||||
"unnamed": "unnamed location — {lat}, {lng}",
|
||||
"close": "Close",
|
||||
"country": "Country",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "unknown",
|
||||
"pgHere": "People groups here",
|
||||
"languages": "Languages",
|
||||
"sheetNote": "A cluster of people-group centroids within ~25 km, not a municipal boundary. Named points come from a cited gazetteer (Nominatim/OSM); unnamed ones show coordinates rather than an invented place name.",
|
||||
"sheetNote": "A cluster of people-group pins within ~25 km, not a municipal boundary. Named points come from a cited gazetteer (Nominatim/OSM); unnamed ones show coordinates rather than an invented place name.",
|
||||
"national": "Internet figures are NATIONAL.",
|
||||
"engineMissing": "The map engine could not be loaded."
|
||||
},
|
||||
"list": {
|
||||
"title": "Key Cities",
|
||||
"intro": "Where the unengaged people groups live: {named} population centres named from a cited gazetteer, across {clusters} located clusters.",
|
||||
"intro": "Clusters of people-group pins: {named} named clusters (names from a cited gazetteer), across {clusters} located clusters.",
|
||||
"filters": {
|
||||
"named": "Named population centres",
|
||||
"multi": "Multi-group clusters",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "people groups: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "What a row is.",
|
||||
"row": "A cluster of people-group centroids within ~25 km, not a municipal boundary. Pins mark where a people group is centred, not a settlement, so clusters are never named by inference: the {n} multi-group centres were resolved by {source} (ODbL, cited per row), and the rest stay unnamed rather than guessed.",
|
||||
"row": "A cluster of people-group pins within ~25 km, not a municipal boundary. Each pin marks a people group, not a town, so clusters are never named by inference: the {n} multi-group centres were resolved by {source} (ODbL, cited per row), and the rest stay unnamed rather than guessed.",
|
||||
"internet": "They are shown as context: per-city connectivity is not publicly available at this scale, and a national figure must not be read as a city measurement.",
|
||||
"scope": "This page carries people-group locations and connectivity only — no church, believer or meeting-point data, by design."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "The dot marks where this people is found, not a town or a boundary.",
|
||||
"sharedPoint": "It sits on the same point as {names} — {n} groups share this coordinate, so the position tells you the locality and not which group is where."
|
||||
"approximate": "The dot marks a people group, not a town or a boundary.",
|
||||
"sharedPoint": "It sits on the same point as {names} — {n} groups share this dot."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/es/blessIndiaPosters.json
Normal file
15
library/i18n/locales/es/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Pósteres «Bendice a la India»",
|
||||
"rangeBadge60x96": "Insignias numeradas · 60×96 (5×8 pies)",
|
||||
"count": "{count} grupos de población",
|
||||
"desktopOnly": "Solo escritorio por ahora: aún no se ha comprobado que este póster grande se imprima de forma segura en un teléfono.",
|
||||
"experimental": "experimental",
|
||||
"intro": "Cada grupo de población de la India en una sola hoja imprimible, generada en vivo y mostrada como sus hojas imprimibles con las tiras de pegado, igual que la vista Tamaños de Poster Design muestra los formatos grandes.",
|
||||
"print": "Imprimir este póster",
|
||||
"attempt": "Insignias numeradas · 60×96 (5×8 pies) · 3 hojas",
|
||||
"stat": "{groups} grupos · {badges} insignias numeradas · 0 solapamientos",
|
||||
"generating": "Generando el póster a partir de los datos en vivo…",
|
||||
"error": "No se pudo generar el póster: {msg}",
|
||||
"photos": "Solo {real} de {total} grupos de la India tienen foto en los datos; un documento impreso las lleva, una imagen plana no. Este diseño numerado imprime los nombres sin fotos para que cada grupo sea igual y legible.",
|
||||
"posterHeader": "Bendice a la India en el nombre de Jesús"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "App de mensajería principal (país)",
|
||||
"messagingNote": "La más descargada en la App Store de Apple del país: solo iOS, un ranking de descargas, no de uso.",
|
||||
"province": "India y China: estado o provincia",
|
||||
"provinceNote": "Aproximado: ubicado según dónde vive un pueblo, no una ciudad.",
|
||||
"provinceNote": "Aproximado: se toma del punto del grupo en el mapa, no de una ciudad.",
|
||||
"region": "Región",
|
||||
"religion": "Religión",
|
||||
"wayIn": "Vía de entrada cuando no hay nada en su idioma"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Mapa de grupos étnicos, sombreado según cuánto usa internet cada país"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "El punto marca dónde vive este pueblo en {country}, no una ciudad.",
|
||||
"pins": "Un punto marca dónde vive un pueblo en su país, no una ciudad. {n} grupos comparten un punto con otro grupo.",
|
||||
"pin": "El punto representa a este pueblo, no una ciudad.",
|
||||
"pins": "Un punto representa un pueblo, no una ciudad. {n} grupos comparten un punto con otro grupo.",
|
||||
"shares": "Comparte su punto con {n} grupo(s) más.",
|
||||
"sources": "Fuentes:",
|
||||
"standing": "Los puntos son aproximados (dónde vive un pueblo en su país, no una ciudad). Las cifras de internet son del país o del estado indio, nunca del grupo. «Encontrado» significa encontrado en las fuentes consultadas."
|
||||
"standing": "Los puntos son aproximados: un punto representa un pueblo, no una ciudad. Las cifras de internet son del país o del estado indio, nunca del grupo. «Encontrado» significa encontrado en las fuentes consultadas."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Gráficos",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Dónde viven los pueblos no alcanzados",
|
||||
"title": "Grupos de marcadores de pueblos",
|
||||
"bands": {
|
||||
"named": "centro con nombre",
|
||||
"multi": "varios pueblos",
|
||||
"single": "un pueblo"
|
||||
},
|
||||
"note": "Un punto es un grupo de centroides de pueblos en ~25 km, no un límite municipal. Los puntos con nombre provienen de un nomenclátor citado.",
|
||||
"note": "Un punto es un grupo de marcadores de pueblos en ~25 km, no un límite municipal. Los puntos con nombre provienen de un nomenclátor citado.",
|
||||
"unnamed": "lugar sin nombre — {lat}, {lng}",
|
||||
"close": "Cerrar",
|
||||
"country": "País",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "desconocido",
|
||||
"pgHere": "Pueblos aquí",
|
||||
"languages": "Idiomas",
|
||||
"sheetNote": "Un grupo de centroides de pueblos en ~25 km, no un límite municipal. Los puntos con nombre provienen de un nomenclátor citado (Nominatim/OSM); los sin nombre muestran coordenadas en lugar de un nombre inventado.",
|
||||
"sheetNote": "Un grupo de marcadores de pueblos en ~25 km, no un límite municipal. Los puntos con nombre provienen de un nomenclátor citado (Nominatim/OSM); los sin nombre muestran coordenadas en lugar de un nombre inventado.",
|
||||
"national": "Las cifras de internet son NACIONALES.",
|
||||
"engineMissing": "No se pudo cargar el motor del mapa."
|
||||
},
|
||||
"list": {
|
||||
"title": "Ciudades clave",
|
||||
"intro": "Dónde viven los pueblos no alcanzados: centros de población con nombre de un nomenclátor citado: {named}, en grupos ubicados: {clusters}.",
|
||||
"intro": "Grupos de marcadores de pueblos: grupos con nombre (de un nomenclátor citado): {named}, en grupos ubicados: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Centros de población con nombre",
|
||||
"multi": "Grupos de varios pueblos",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "pueblos: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Qué es una fila.",
|
||||
"row": "Un grupo de centroides de pueblos en ~25 km, no un límite municipal. Los puntos marcan dónde se centra un pueblo, no un asentamiento, así que los grupos nunca se nombran por deducción: los {n} centros con varios pueblos se identificaron con {source} (ODbL, citado en cada fila) y el resto queda sin nombre en lugar de adivinarlo.",
|
||||
"row": "Un grupo de marcadores de pueblos en ~25 km, no un límite municipal. Cada marcador representa un pueblo, no una ciudad, así que los grupos nunca se nombran por deducción: los {n} centros con varios pueblos se identificaron con {source} (ODbL, citado en cada fila) y el resto queda sin nombre en lugar de adivinarlo.",
|
||||
"internet": "Se muestran como contexto: la conectividad por ciudad no está disponible públicamente a esta escala, y una cifra nacional no debe leerse como una medición de la ciudad.",
|
||||
"scope": "Esta página contiene solo ubicaciones de pueblos y conectividad; a propósito, no hay datos de iglesias, creyentes ni lugares de reunión."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "El punto indica dónde se encuentra este pueblo, no una ciudad ni una frontera.",
|
||||
"sharedPoint": "Está en el mismo punto que {names}: {n} grupos comparten esta coordenada, así que la posición indica la zona y no dónde está cada grupo."
|
||||
"approximate": "El punto representa un pueblo, no una ciudad ni una frontera.",
|
||||
"sharedPoint": "Está en el mismo punto que {names}: {n} grupos comparten este punto."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/fr/blessIndiaPosters.json
Normal file
15
library/i18n/locales/fr/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Affiches « Bénis l’Inde »",
|
||||
"rangeBadge60x96": "Badges de numéros · 60×96 (5×8 pi)",
|
||||
"count": "{count} groupes de population",
|
||||
"desktopOnly": "Ordinateur uniquement pour l’instant — il n’est pas encore prouvé que cette grande affiche s’imprime correctement sur un téléphone.",
|
||||
"experimental": "expérimental",
|
||||
"intro": "Chaque groupe de population de l'Inde sur une seule feuille imprimable, générée en direct et montrée comme ses feuilles imprimables avec les bandes de collage — comme la vue Tailles de Poster Design montre les grands formats.",
|
||||
"print": "Imprimer cette affiche",
|
||||
"attempt": "Badges de numéros · 60×96 (5×8 pi) · 3 feuilles",
|
||||
"stat": "{groups} groupes · {badges} badges de numéros · 0 chevauchement",
|
||||
"generating": "Génération de l'affiche à partir des données en direct…",
|
||||
"error": "Impossible de générer l'affiche : {msg}",
|
||||
"photos": "Seulement {real} des {total} groupes de l'Inde ont une photo dans les données ; un document imprimé les porte, une image plate non. Ce modèle numéroté imprime les noms sans photos pour que chaque groupe soit égal et lisible.",
|
||||
"posterHeader": "Bénissez l'Inde au nom de Jésus"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "Messagerie principale (pays)",
|
||||
"messagingNote": "La plus téléchargée sur l’App Store d’Apple du pays : iOS seulement, un rang de téléchargements, pas l’usage.",
|
||||
"province": "Inde et Chine : État ou province",
|
||||
"provinceNote": "Approximatif : placé d’après l’endroit où vit un peuple, pas une ville.",
|
||||
"provinceNote": "Approximatif : déduit du point du groupe sur la carte, pas d’une ville.",
|
||||
"region": "Région",
|
||||
"religion": "Religion",
|
||||
"wayIn": "Porte d’entrée quand rien n’existe dans leur langue"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Carte des groupes ethniques, ombrée selon la part de chaque pays qui utilise internet"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "Le point indique où ce peuple vit en {country}, pas une ville.",
|
||||
"pins": "Un point indique où un peuple vit dans son pays, pas une ville. {n} groupes partagent un point avec un autre groupe.",
|
||||
"pin": "Le point représente ce peuple, pas une ville.",
|
||||
"pins": "Un point représente un peuple, pas une ville. {n} groupes partagent un point avec un autre groupe.",
|
||||
"shares": "Il partage son point avec {n} autre(s) groupe(s).",
|
||||
"sources": "Sources :",
|
||||
"standing": "Les points sont approximatifs (où un peuple vit dans son pays, pas une ville). Les chiffres d’internet sont ceux du pays ou de l’État indien, jamais du groupe. « Trouvé » signifie trouvé dans les sources consultées."
|
||||
"standing": "Les points sont approximatifs : un point représente un peuple, pas une ville. Les chiffres d’internet sont ceux du pays ou de l’État indien, jamais du groupe. « Trouvé » signifie trouvé dans les sources consultées."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Graphiques",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Où vivent les peuples non atteints",
|
||||
"title": "Regroupements de repères de peuples",
|
||||
"bands": {
|
||||
"named": "centre nommé",
|
||||
"multi": "plusieurs peuples",
|
||||
"single": "un seul peuple"
|
||||
},
|
||||
"note": "Un point est un regroupement de centroïdes de peuples dans un rayon d’environ 25 km, pas une limite municipale. Les points nommés proviennent d’un répertoire géographique cité.",
|
||||
"note": "Un point est un regroupement de repères de peuples dans un rayon d’environ 25 km, pas une limite municipale. Les points nommés proviennent d’un répertoire géographique cité.",
|
||||
"unnamed": "lieu sans nom — {lat}, {lng}",
|
||||
"close": "Fermer",
|
||||
"country": "Pays",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "inconnu",
|
||||
"pgHere": "Peuples ici",
|
||||
"languages": "Langues",
|
||||
"sheetNote": "Un regroupement de centroïdes de peuples dans un rayon d’environ 25 km, pas une limite municipale. Les points nommés proviennent d’un répertoire géographique cité (Nominatim/OSM) ; les points sans nom affichent des coordonnées plutôt qu’un nom inventé.",
|
||||
"sheetNote": "Un regroupement de repères de peuples dans un rayon d’environ 25 km, pas une limite municipale. Les points nommés proviennent d’un répertoire géographique cité (Nominatim/OSM) ; les points sans nom affichent des coordonnées plutôt qu’un nom inventé.",
|
||||
"national": "Les chiffres Internet sont NATIONAUX.",
|
||||
"engineMissing": "Le moteur de carte n’a pas pu être chargé."
|
||||
},
|
||||
"list": {
|
||||
"title": "Villes clés",
|
||||
"intro": "Où vivent les peuples non atteints : centres de population nommés d’après un répertoire cité : {named}, sur des regroupements localisés : {clusters}.",
|
||||
"intro": "Regroupements de repères de peuples : regroupements nommés (d’après un répertoire cité) : {named}, sur des regroupements localisés : {clusters}.",
|
||||
"filters": {
|
||||
"named": "Centres de population nommés",
|
||||
"multi": "Regroupements de plusieurs peuples",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "peuples : {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Ce qu’est une ligne.",
|
||||
"row": "Un regroupement de centroïdes de peuples dans un rayon d’environ 25 km, pas une limite municipale. Les points indiquent où un peuple est centré, pas une localité, donc les regroupements ne sont jamais nommés par déduction : les {n} centres à plusieurs peuples ont été identifiés avec {source} (ODbL, cité à chaque ligne) et les autres restent sans nom plutôt que devinés.",
|
||||
"row": "Un regroupement de repères de peuples dans un rayon d’environ 25 km, pas une limite municipale. Chaque repère représente un peuple, pas une ville, donc les regroupements ne sont jamais nommés par déduction : les {n} centres à plusieurs peuples ont été identifiés avec {source} (ODbL, cité à chaque ligne) et les autres restent sans nom plutôt que devinés.",
|
||||
"internet": "Ils sont donnés comme contexte : la connectivité par ville n’est pas publique à cette échelle, et un chiffre national ne doit pas être lu comme une mesure de la ville.",
|
||||
"scope": "Cette page ne contient que les emplacements des peuples et la connectivité — aucune donnée sur les églises, les croyants ou les lieux de réunion, à dessein."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "Le point indique où vit ce peuple, pas une ville ni une frontière.",
|
||||
"sharedPoint": "Il se trouve au même point que {names} — {n} groupes partagent cette coordonnée : la position indique la région, pas où se trouve chaque groupe."
|
||||
"approximate": "Le point représente un peuple, pas une ville ni une frontière.",
|
||||
"sharedPoint": "Il se trouve au même point que {names} — {n} groupes partagent ce point."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/hi/blessIndiaPosters.json
Normal file
15
library/i18n/locales/hi/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "भारत को आशीष दें — पोस्टर",
|
||||
"rangeBadge60x96": "क्रमांकित बैज · 60×96 (5×8 फुट)",
|
||||
"count": "{count} लोग समूह",
|
||||
"desktopOnly": "अभी केवल डेस्कटॉप — यह बड़ा पोस्टर अभी फ़ोन पर सुरक्षित रूप से प्रिंट होना सिद्ध नहीं हुआ है।",
|
||||
"experimental": "प्रायोगिक",
|
||||
"intro": "भारत का हर लोग-समूह एक ही प्रिंट-योग्य शीट पर, लाइव डेटा से बनाया गया और गोंद-पट्टियों के साथ अपनी प्रिंट-योग्य शीटों के रूप में दिखाया गया — जैसे Poster Design का Sizes व्यू बड़े फ़ॉर्मैट दिखाता है।",
|
||||
"print": "यह पोस्टर प्रिंट करें",
|
||||
"attempt": "रेंज बैज · 60×96 (5×8 फुट) · 3 शीट",
|
||||
"stat": "{groups} समूह · {badges} रेंज बैज · 0 ओवरलैप",
|
||||
"generating": "लाइव डेटा से पोस्टर बनाया जा रहा है…",
|
||||
"error": "पोस्टर नहीं बन सका: {msg}",
|
||||
"photos": "डेटा में {total} में से केवल {real} भारतीय समूहों के पास फ़ोटो है; मुद्रित दस्तावेज़ उन्हें ले जाता है, सपाट छवि नहीं। यह क्रमांकित डिज़ाइन बिना फ़ोटो के नाम छापता है ताकि हर समूह समान और सुपाठ्य हो।",
|
||||
"posterHeader": "यीशु के नाम में भारत को आशीष दें"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "प्रमुख मैसेजिंग ऐप (देश)",
|
||||
"messagingNote": "देश के Apple App Store में सबसे अधिक डाउनलोड किया गया: केवल iOS, डाउनलोड रैंक, उपयोग नहीं।",
|
||||
"province": "भारत और चीन: राज्य या प्रांत",
|
||||
"provinceNote": "अनुमानित: जहाँ जन-समूह पाया जाता है उसके आधार पर, किसी कस्बे के आधार पर नहीं।",
|
||||
"provinceNote": "अनुमानित: मानचित्र पर समूह के बिंदु से लिया गया, किसी कस्बे से नहीं।",
|
||||
"region": "क्षेत्र",
|
||||
"religion": "धर्म",
|
||||
"wayIn": "जब उनकी भाषा में कुछ न हो, तब का रास्ता"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "जन-समूहों का मानचित्र, हर देश में इंटरनेट उपयोग के अनुसार रंगा हुआ"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "बिंदु दिखाता है कि यह जन-समूह {country} में कहाँ पाया जाता है, कोई कस्बा नहीं।",
|
||||
"pins": "बिंदु दिखाता है कि कोई जन-समूह अपने देश में कहाँ पाया जाता है, कोई कस्बा नहीं। {n} समूह किसी दूसरे समूह के साथ एक ही बिंदु साझा करते हैं।",
|
||||
"pin": "बिंदु इस जन-समूह को दर्शाता है, किसी कस्बे को नहीं।",
|
||||
"pins": "बिंदु किसी जन-समूह को दर्शाता है, किसी कस्बे को नहीं। {n} समूह किसी दूसरे समूह के साथ एक ही बिंदु साझा करते हैं।",
|
||||
"shares": "यह अपना बिंदु {n} अन्य समूह(ों) के साथ साझा करता है।",
|
||||
"sources": "स्रोत:",
|
||||
"standing": "बिंदु अनुमानित हैं (जहाँ जन-समूह अपने देश में पाया जाता है, कोई कस्बा नहीं)। इंटरनेट के आंकड़े देश या भारतीय राज्य के हैं, समूह के कभी नहीं। \"मिला\" का अर्थ है जाँचे गए स्रोतों में मिला।"
|
||||
"standing": "बिंदु अनुमानित हैं: बिंदु किसी जन-समूह को दर्शाता है, किसी कस्बे को नहीं। इंटरनेट के आंकड़े देश या भारतीय राज्य के हैं, समूह के कभी नहीं। \"मिला\" का अर्थ है जाँचे गए स्रोतों में मिला।"
|
||||
},
|
||||
"panel": {
|
||||
"charts": "चार्ट",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "अपहुँचे जनसमूह कहाँ रहते हैं",
|
||||
"title": "जनसमूह बिंदुओं के समूह",
|
||||
"bands": {
|
||||
"named": "नामित केंद्र",
|
||||
"multi": "कई जनसमूह",
|
||||
"single": "एक जनसमूह"
|
||||
},
|
||||
"note": "एक बिंदु लगभग 25 किमी के भीतर जनसमूह केंद्रों का समूह है, नगरपालिका सीमा नहीं। नामित बिंदु एक उद्धृत भौगोलिक सूची से लिए गए हैं।",
|
||||
"note": "एक बिंदु लगभग 25 किमी के भीतर जनसमूह बिंदुओं का समूह है, नगरपालिका सीमा नहीं। नामित बिंदु एक उद्धृत भौगोलिक सूची से लिए गए हैं।",
|
||||
"unnamed": "बिना नाम का स्थान — {lat}, {lng}",
|
||||
"close": "बंद करें",
|
||||
"country": "देश",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "अज्ञात",
|
||||
"pgHere": "यहाँ के जनसमूह",
|
||||
"languages": "भाषाएँ",
|
||||
"sheetNote": "लगभग 25 किमी के भीतर जनसमूह केंद्रों का समूह, नगरपालिका सीमा नहीं। नामित बिंदु एक उद्धृत भौगोलिक सूची (Nominatim/OSM) से हैं; बिना नाम वाले बिंदु गढ़े हुए नाम के बजाय निर्देशांक दिखाते हैं।",
|
||||
"sheetNote": "लगभग 25 किमी के भीतर जनसमूह बिंदुओं का समूह, नगरपालिका सीमा नहीं। नामित बिंदु एक उद्धृत भौगोलिक सूची (Nominatim/OSM) से हैं; बिना नाम वाले बिंदु गढ़े हुए नाम के बजाय निर्देशांक दिखाते हैं।",
|
||||
"national": "इंटरनेट के आँकड़े राष्ट्रीय हैं।",
|
||||
"engineMissing": "नक्शे का इंजन लोड नहीं हो सका।"
|
||||
},
|
||||
"list": {
|
||||
"title": "मुख्य शहर",
|
||||
"intro": "अपहुँचे जनसमूह कहाँ रहते हैं: उद्धृत भौगोलिक सूची से नामित जनसंख्या केंद्र: {named}, स्थित समूहों में: {clusters}।",
|
||||
"intro": "जनसमूह बिंदुओं के समूह: नामित समूह (नाम उद्धृत भौगोलिक सूची से): {named}, स्थित समूहों में: {clusters}।",
|
||||
"filters": {
|
||||
"named": "नामित जनसंख्या केंद्र",
|
||||
"multi": "कई जनसमूहों वाले समूह",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "जनसमूह: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "एक पंक्ति क्या है।",
|
||||
"row": "लगभग 25 किमी के भीतर जनसमूह केंद्रों का समूह, नगरपालिका सीमा नहीं। बिंदु दिखाते हैं कि कोई जनसमूह कहाँ केंद्रित है, कोई बस्ती नहीं, इसलिए समूहों का नाम कभी अनुमान से नहीं रखा जाता: कई जनसमूहों वाले {n} केंद्र {source} (ODbL, हर पंक्ति में उद्धृत) से पहचाने गए, और बाकी अनुमान के बजाय बिना नाम के रहते हैं।",
|
||||
"row": "लगभग 25 किमी के भीतर जनसमूह बिंदुओं का समूह, नगरपालिका सीमा नहीं। हर बिंदु एक जनसमूह को दर्शाता है, किसी कस्बे को नहीं, इसलिए समूहों का नाम कभी अनुमान से नहीं रखा जाता: कई जनसमूहों वाले {n} केंद्र {source} (ODbL, हर पंक्ति में उद्धृत) से पहचाने गए, और बाकी अनुमान के बजाय बिना नाम के रहते हैं।",
|
||||
"internet": "इन्हें संदर्भ के रूप में दिखाया गया है: इस स्तर पर शहर-वार कनेक्टिविटी सार्वजनिक रूप से उपलब्ध नहीं है, और राष्ट्रीय आँकड़े को शहर का माप नहीं समझना चाहिए।",
|
||||
"scope": "इस पृष्ठ पर केवल जनसमूहों के स्थान और कनेक्टिविटी है — जानबूझकर कलीसिया, विश्वासियों या मिलने के स्थानों का कोई डेटा नहीं।"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "बिंदु दिखाता है कि यह लोग कहाँ पाए जाते हैं, कोई शहर या सीमा नहीं।",
|
||||
"sharedPoint": "यह {names} के साथ एक ही बिंदु पर है — {n} समूह यह निर्देशांक साझा करते हैं, इसलिए स्थान क्षेत्र बताता है, यह नहीं कि कौन-सा समूह कहाँ है।"
|
||||
"approximate": "बिंदु एक जन-समूह को दर्शाता है, किसी शहर या सीमा को नहीं।",
|
||||
"sharedPoint": "यह {names} के साथ एक ही बिंदु पर है — {n} समूह यह बिंदु साझा करते हैं।"
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/it/blessIndiaPosters.json
Normal file
15
library/i18n/locales/it/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Poster «Benedici l’India»",
|
||||
"rangeBadge60x96": "Badge numerati · 60×96 (5×8 ft)",
|
||||
"count": "{count} gruppi di popolazione",
|
||||
"desktopOnly": "Solo desktop per ora — non è ancora dimostrato che questo grande poster si stampi in sicurezza su un telefono.",
|
||||
"experimental": "sperimentale",
|
||||
"intro": "Ogni gruppo di popolazione dell'India su un unico foglio stampabile, generato dal vivo e mostrato come i suoi fogli stampabili con le strisce di colla — come la vista Dimensioni di Poster Design mostra i grandi formati.",
|
||||
"print": "Stampa questo poster",
|
||||
"attempt": "Badge numerati · 60×96 (5×8 ft) · 3 fogli",
|
||||
"stat": "{groups} gruppi · {badges} badge numerati · 0 sovrapposizioni",
|
||||
"generating": "Generazione del poster dai dati dal vivo…",
|
||||
"error": "Impossibile generare il poster: {msg}",
|
||||
"photos": "Solo {real} di {total} gruppi dell'India hanno una foto nei dati; un documento stampato le porta, un'immagine piatta no. Questo design numerato stampa i nomi senza foto così ogni gruppo è uguale e leggibile.",
|
||||
"posterHeader": "Benedici l'India nel nome di Gesù"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "App di messaggistica principale (paese)",
|
||||
"messagingNote": "La più scaricata nell’App Store di Apple del paese: solo iOS, una classifica di download, non di uso.",
|
||||
"province": "India e Cina: stato o provincia",
|
||||
"provinceNote": "Approssimativo: collocato dove vive un popolo, non in una città.",
|
||||
"provinceNote": "Approssimativo: ricavato dal punto del gruppo sulla mappa, non da una città.",
|
||||
"region": "Regione",
|
||||
"religion": "Religione",
|
||||
"wayIn": "Via d’accesso quando non c’è niente nella loro lingua"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Mappa dei gruppi etnici, ombreggiata secondo quanto ogni paese usa internet"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "Il punto indica dove vive questo popolo in {country}, non una città.",
|
||||
"pins": "Un punto indica dove vive un popolo nel suo paese, non una città. {n} gruppi condividono un punto con un altro gruppo.",
|
||||
"pin": "Il punto rappresenta questo popolo, non una città.",
|
||||
"pins": "Un punto rappresenta un popolo, non una città. {n} gruppi condividono un punto con un altro gruppo.",
|
||||
"shares": "Condivide il punto con {n} altro/i gruppo/i.",
|
||||
"sources": "Fonti:",
|
||||
"standing": "I punti sono approssimativi (dove vive un popolo nel suo paese, non una città). I dati di internet sono del paese o dello stato indiano, mai del gruppo. «Trovato» significa trovato nelle fonti consultate."
|
||||
"standing": "I punti sono approssimativi: un punto rappresenta un popolo, non una città. I dati di internet sono del paese o dello stato indiano, mai del gruppo. «Trovato» significa trovato nelle fonti consultate."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Grafici",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Dove vivono i popoli non raggiunti",
|
||||
"title": "Raggruppamenti di segnaposto di popoli",
|
||||
"bands": {
|
||||
"named": "centro con nome",
|
||||
"multi": "più popoli",
|
||||
"single": "un solo popolo"
|
||||
},
|
||||
"note": "Un punto è un raggruppamento di centroidi di popoli entro ~25 km, non un confine comunale. I punti con nome provengono da un gazetteer citato.",
|
||||
"note": "Un punto è un raggruppamento di segnaposto di popoli entro ~25 km, non un confine comunale. I punti con nome provengono da un gazetteer citato.",
|
||||
"unnamed": "luogo senza nome — {lat}, {lng}",
|
||||
"close": "Chiudi",
|
||||
"country": "Paese",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "sconosciuto",
|
||||
"pgHere": "Popoli qui",
|
||||
"languages": "Lingue",
|
||||
"sheetNote": "Un raggruppamento di centroidi di popoli entro ~25 km, non un confine comunale. I punti con nome provengono da un gazetteer citato (Nominatim/OSM); quelli senza nome mostrano le coordinate invece di un nome inventato.",
|
||||
"sheetNote": "Un raggruppamento di segnaposto di popoli entro ~25 km, non un confine comunale. I punti con nome provengono da un gazetteer citato (Nominatim/OSM); quelli senza nome mostrano le coordinate invece di un nome inventato.",
|
||||
"national": "I dati su Internet sono NAZIONALI.",
|
||||
"engineMissing": "Impossibile caricare il motore della mappa."
|
||||
},
|
||||
"list": {
|
||||
"title": "Città chiave",
|
||||
"intro": "Dove vivono i popoli non raggiunti: centri abitati con nome da un gazetteer citato: {named}, in raggruppamenti localizzati: {clusters}.",
|
||||
"intro": "Raggruppamenti di segnaposto di popoli: raggruppamenti con nome (da un gazetteer citato): {named}, in raggruppamenti localizzati: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Centri abitati con nome",
|
||||
"multi": "Raggruppamenti di più popoli",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "popoli: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Che cos’è una riga.",
|
||||
"row": "Un raggruppamento di centroidi di popoli entro ~25 km, non un confine comunale. I punti indicano dove si concentra un popolo, non un insediamento, quindi i raggruppamenti non vengono mai nominati per deduzione: i {n} centri con più popoli sono stati identificati con {source} (ODbL, citato in ogni riga) e gli altri restano senza nome invece di essere indovinati.",
|
||||
"row": "Un raggruppamento di segnaposto di popoli entro ~25 km, non un confine comunale. Ogni segnaposto rappresenta un popolo, non una città, quindi i raggruppamenti non vengono mai nominati per deduzione: i {n} centri con più popoli sono stati identificati con {source} (ODbL, citato in ogni riga) e gli altri restano senza nome invece di essere indovinati.",
|
||||
"internet": "Sono mostrati come contesto: la connettività per città non è pubblicamente disponibile a questa scala, e un dato nazionale non va letto come una misura della città.",
|
||||
"scope": "Questa pagina contiene solo le posizioni dei popoli e la connettività — nessun dato su chiese, credenti o luoghi di incontro, per scelta."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "Il punto indica dove si trova questo popolo, non una città né un confine.",
|
||||
"sharedPoint": "Si trova nello stesso punto di {names} — {n} gruppi condividono questa coordinata, quindi la posizione indica la zona e non dove si trova ciascun gruppo."
|
||||
"approximate": "Il punto rappresenta un popolo, non una città né un confine.",
|
||||
"sharedPoint": "Si trova nello stesso punto di {names} — {n} gruppi condividono questo punto."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/pt/blessIndiaPosters.json
Normal file
15
library/i18n/locales/pt/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Pôsteres «Abençoe a Índia»",
|
||||
"rangeBadge60x96": "Emblemas numerados · 60×96 (5×8 pés)",
|
||||
"count": "{count} grupos populacionais",
|
||||
"desktopOnly": "Apenas no computador por enquanto — ainda não se comprovou que este pôster grande imprime com segurança num telemóvel.",
|
||||
"experimental": "experimental",
|
||||
"intro": "Cada grupo populacional da Índia numa única folha imprimível, gerada ao vivo e mostrada como as suas folhas imprimíveis com as tiras de cola — como a vista Tamanhos do Poster Design mostra os formatos grandes.",
|
||||
"print": "Imprimir este pôster",
|
||||
"attempt": "Emblemas numerados · 60×96 (5×8 pés) · 3 folhas",
|
||||
"stat": "{groups} grupos · {badges} emblemas numerados · 0 sobreposições",
|
||||
"generating": "A gerar o pôster a partir dos dados ao vivo…",
|
||||
"error": "Não foi possível gerar o pôster: {msg}",
|
||||
"photos": "Apenas {real} de {total} grupos da Índia têm foto nos dados; um documento impresso leva-as, uma imagem plana não. Este design numerado imprime os nomes sem fotos para que cada grupo seja igual e legível.",
|
||||
"posterHeader": "Abençoe a Índia em nome de Jesus"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "App de mensagens principal (país)",
|
||||
"messagingNote": "O mais baixado na App Store da Apple do país: só iOS, um ranking de downloads, não de uso.",
|
||||
"province": "Índia e China: estado ou província",
|
||||
"provinceNote": "Aproximado: colocado onde um povo vive, não numa cidade.",
|
||||
"provinceNote": "Aproximado: obtido a partir do ponto do grupo no mapa, não de uma cidade.",
|
||||
"region": "Região",
|
||||
"religion": "Religião",
|
||||
"wayIn": "Caminho de entrada quando não há nada na língua deles"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Mapa dos povos, sombreado pelo quanto cada país usa a internet"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "O ponto marca onde este povo vive em {country}, não uma cidade.",
|
||||
"pins": "Um ponto marca onde um povo vive no seu país, não uma cidade. {n} grupos dividem um ponto com outro grupo.",
|
||||
"pin": "O ponto representa este povo, não uma cidade.",
|
||||
"pins": "Um ponto representa um povo, não uma cidade. {n} grupos dividem um ponto com outro grupo.",
|
||||
"shares": "Divide o ponto com {n} outro(s) grupo(s).",
|
||||
"sources": "Fontes:",
|
||||
"standing": "Os pontos são aproximados (onde um povo vive no seu país, não uma cidade). Os números de internet são do país ou do estado indiano, nunca do grupo. “Encontrado” significa encontrado nas fontes verificadas."
|
||||
"standing": "Os pontos são aproximados: um ponto representa um povo, não uma cidade. Os números de internet são do país ou do estado indiano, nunca do grupo. “Encontrado” significa encontrado nas fontes verificadas."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Gráficos",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Onde vivem os povos não alcançados",
|
||||
"title": "Agrupamentos de marcadores de povos",
|
||||
"bands": {
|
||||
"named": "centro com nome",
|
||||
"multi": "vários povos",
|
||||
"single": "um povo"
|
||||
},
|
||||
"note": "Um ponto é um agrupamento de centroides de povos num raio de ~25 km, não um limite municipal. Os pontos com nome vêm de um gazetteer citado.",
|
||||
"note": "Um ponto é um agrupamento de marcadores de povos num raio de ~25 km, não um limite municipal. Os pontos com nome vêm de um gazetteer citado.",
|
||||
"unnamed": "local sem nome — {lat}, {lng}",
|
||||
"close": "Fechar",
|
||||
"country": "País",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "desconhecido",
|
||||
"pgHere": "Povos aqui",
|
||||
"languages": "Idiomas",
|
||||
"sheetNote": "Um agrupamento de centroides de povos num raio de ~25 km, não um limite municipal. Os pontos com nome vêm de um gazetteer citado (Nominatim/OSM); os sem nome mostram coordenadas em vez de um nome inventado.",
|
||||
"sheetNote": "Um agrupamento de marcadores de povos num raio de ~25 km, não um limite municipal. Os pontos com nome vêm de um gazetteer citado (Nominatim/OSM); os sem nome mostram coordenadas em vez de um nome inventado.",
|
||||
"national": "Os números de internet são NACIONAIS.",
|
||||
"engineMissing": "Não foi possível carregar o motor do mapa."
|
||||
},
|
||||
"list": {
|
||||
"title": "Cidades-chave",
|
||||
"intro": "Onde vivem os povos não alcançados: centros populacionais com nome de um gazetteer citado: {named}, em agrupamentos localizados: {clusters}.",
|
||||
"intro": "Agrupamentos de marcadores de povos: agrupamentos com nome (de um gazetteer citado): {named}, em agrupamentos localizados: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Centros populacionais com nome",
|
||||
"multi": "Agrupamentos de vários povos",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "povos: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "O que é uma linha.",
|
||||
"row": "Um agrupamento de centroides de povos num raio de ~25 km, não um limite municipal. Os pontos marcam onde um povo se concentra, não um povoado, por isso os agrupamentos nunca são nomeados por dedução: os {n} centros com vários povos foram identificados com {source} (ODbL, citado em cada linha) e os restantes ficam sem nome em vez de adivinhados.",
|
||||
"row": "Um agrupamento de marcadores de povos num raio de ~25 km, não um limite municipal. Cada marcador representa um povo, não uma cidade, por isso os agrupamentos nunca são nomeados por dedução: os {n} centros com vários povos foram identificados com {source} (ODbL, citado em cada linha) e os restantes ficam sem nome em vez de adivinhados.",
|
||||
"internet": "São mostrados como contexto: a conectividade por cidade não está disponível publicamente nesta escala, e um número nacional não deve ser lido como uma medição da cidade.",
|
||||
"scope": "Esta página contém apenas localizações de povos e conectividade — por opção, nenhum dado sobre igrejas, crentes ou locais de reunião."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "O ponto indica onde este povo se encontra, não uma cidade nem uma fronteira.",
|
||||
"sharedPoint": "Está no mesmo ponto que {names} — {n} grupos partilham esta coordenada, por isso a posição indica a região e não onde está cada grupo."
|
||||
"approximate": "O ponto representa um povo, não uma cidade nem uma fronteira.",
|
||||
"sharedPoint": "Está no mesmo ponto que {names} — {n} grupos partilham este ponto."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/ro/blessIndiaPosters.json
Normal file
15
library/i18n/locales/ro/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Postere „Binecuvântează India”",
|
||||
"rangeBadge60x96": "Insigne numerotate · 60×96 (5×8 ft)",
|
||||
"count": "{count} grupuri de popoare",
|
||||
"desktopOnly": "Doar pe desktop deocamdată — nu s-a dovedit încă că acest poster mare se tipărește în siguranță pe un telefon.",
|
||||
"experimental": "experimental",
|
||||
"intro": "Fiecare grup de popoare din India pe o singură coală imprimabilă, generată live și afișată ca foile sale imprimabile cu benzile de lipici — așa cum vizualizarea Dimensiuni din Poster Design arată formatele mari.",
|
||||
"print": "Imprimă acest poster",
|
||||
"attempt": "Insigne numerotate · 60×96 (5×8 ft) · 3 foi",
|
||||
"stat": "{groups} grupuri · {badges} insigne numerotate · 0 suprapuneri",
|
||||
"generating": "Se generează posterul din datele live…",
|
||||
"error": "Nu s-a putut genera posterul: {msg}",
|
||||
"photos": "Doar {real} din {total} grupuri din India au o fotografie în date; un document tipărit le poartă, o imagine plană nu. Acest design numerotat tipărește numele fără fotografii, astfel încât fiecare grup să fie egal și lizibil.",
|
||||
"posterHeader": "Binecuvântează India în numele lui Isus"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "Aplicația de mesagerie principală (țară)",
|
||||
"messagingNote": "Cea mai descărcată în App Store-ul Apple al țării: doar iOS, un clasament de descărcări, nu de utilizare.",
|
||||
"province": "India și China: stat sau provincie",
|
||||
"provinceNote": "Aproximativ: plasat după locul unde trăiește un popor, nu un oraș.",
|
||||
"provinceNote": "Aproximativ: dedus din punctul grupului pe hartă, nu dintr-un oraș.",
|
||||
"region": "Regiune",
|
||||
"religion": "Religie",
|
||||
"wayIn": "Cale de acces când nu există nimic în limba lor"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Harta grupurilor etnice, colorată după cât folosește internetul fiecare țară"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "Punctul arată unde trăiește acest popor în {country}, nu un oraș.",
|
||||
"pins": "Un punct arată unde trăiește un popor în țara sa, nu un oraș. {n} grupuri împart un punct cu alt grup.",
|
||||
"pin": "Punctul reprezintă acest popor, nu un oraș.",
|
||||
"pins": "Un punct reprezintă un popor, nu un oraș. {n} grupuri împart un punct cu alt grup.",
|
||||
"shares": "Își împarte punctul cu alte {n} grup(uri).",
|
||||
"sources": "Surse:",
|
||||
"standing": "Punctele sunt aproximative (unde trăiește un popor în țara sa, nu un oraș). Cifrele de internet sunt ale țării sau ale statului indian, niciodată ale grupului. „Găsit” înseamnă găsit în sursele verificate."
|
||||
"standing": "Punctele sunt aproximative: un punct reprezintă un popor, nu un oraș. Cifrele de internet sunt ale țării sau ale statului indian, niciodată ale grupului. „Găsit” înseamnă găsit în sursele verificate."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Grafice",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Unde trăiesc popoarele neatinse",
|
||||
"title": "Grupuri de marcaje ale popoarelor",
|
||||
"bands": {
|
||||
"named": "centru numit",
|
||||
"multi": "mai multe popoare",
|
||||
"single": "un singur popor"
|
||||
},
|
||||
"note": "Un punct este un grup de centroizi ai popoarelor pe o rază de ~25 km, nu o limită municipală. Punctele numite provin dintr-un gazetteer citat.",
|
||||
"note": "Un punct este un grup de marcaje ale popoarelor pe o rază de ~25 km, nu o limită municipală. Punctele numite provin dintr-un gazetteer citat.",
|
||||
"unnamed": "loc fără nume — {lat}, {lng}",
|
||||
"close": "Închide",
|
||||
"country": "Țară",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "necunoscut",
|
||||
"pgHere": "Popoare aici",
|
||||
"languages": "Limbi",
|
||||
"sheetNote": "Un grup de centroizi ai popoarelor pe o rază de ~25 km, nu o limită municipală. Punctele numite provin dintr-un gazetteer citat (Nominatim/OSM); cele fără nume arată coordonate în loc de un nume inventat.",
|
||||
"sheetNote": "Un grup de marcaje ale popoarelor pe o rază de ~25 km, nu o limită municipală. Punctele numite provin dintr-un gazetteer citat (Nominatim/OSM); cele fără nume arată coordonate în loc de un nume inventat.",
|
||||
"national": "Cifrele de internet sunt NAȚIONALE.",
|
||||
"engineMissing": "Motorul hărții nu a putut fi încărcat."
|
||||
},
|
||||
"list": {
|
||||
"title": "Orașe-cheie",
|
||||
"intro": "Unde trăiesc popoarele neatinse: centre de populație numite dintr-un gazetteer citat: {named}, în grupuri localizate: {clusters}.",
|
||||
"intro": "Grupuri de marcaje ale popoarelor: grupuri numite (dintr-un gazetteer citat): {named}, în grupuri localizate: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Centre de populație numite",
|
||||
"multi": "Grupuri cu mai multe popoare",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "popoare: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Ce este un rând.",
|
||||
"row": "Un grup de centroizi ai popoarelor pe o rază de ~25 km, nu o limită municipală. Punctele arată unde este centrat un popor, nu o așezare, așa că grupurile nu sunt numite niciodată prin deducție: cele {n} centre cu mai multe popoare au fost identificate prin {source} (ODbL, citat pe fiecare rând), iar restul rămân fără nume în loc să fie ghicite.",
|
||||
"row": "Un grup de marcaje ale popoarelor pe o rază de ~25 km, nu o limită municipală. Fiecare marcaj reprezintă un popor, nu un oraș, așa că grupurile nu sunt numite niciodată prin deducție: cele {n} centre cu mai multe popoare au fost identificate prin {source} (ODbL, citat pe fiecare rând), iar restul rămân fără nume în loc să fie ghicite.",
|
||||
"internet": "Sunt afișate drept context: conectivitatea pe oraș nu este disponibilă public la această scară, iar o cifră națională nu trebuie citită ca măsurătoare a orașului.",
|
||||
"scope": "Această pagină conține doar locațiile popoarelor și conectivitatea — intenționat, fără date despre biserici, credincioși sau locuri de întâlnire."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "Punctul arată unde se află acest popor, nu un oraș sau o graniță.",
|
||||
"sharedPoint": "Se află în același punct cu {names} — {n} grupuri împart această coordonată, deci poziția arată zona, nu unde se află fiecare grup."
|
||||
"approximate": "Punctul reprezintă un popor, nu un oraș sau o graniță.",
|
||||
"sharedPoint": "Se află în același punct cu {names} — {n} grupuri împart acest punct."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/ru/blessIndiaPosters.json
Normal file
15
library/i18n/locales/ru/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "Плакаты «Благослови Индию»",
|
||||
"rangeBadge60x96": "Номерные значки · 60×96 (5×8 футов)",
|
||||
"count": "{count} народностей",
|
||||
"desktopOnly": "Пока только на компьютере — ещё не доказано, что этот большой плакат безопасно печатается на телефоне.",
|
||||
"experimental": "экспериментально",
|
||||
"intro": "Каждая народность Индии на одном печатном листе, создаётся вживую и показывается как её печатные листы с полосами склейки — так же, как представление «Размеры» в Poster Design показывает большие форматы.",
|
||||
"print": "Напечатать этот плакат",
|
||||
"attempt": "Номерные значки · 60×96 (5×8 футов) · 3 листа",
|
||||
"stat": "{groups} народностей · {badges} номерных значков · 0 наложений",
|
||||
"generating": "Плакат создаётся по «живым» данным…",
|
||||
"error": "Не удалось создать плакат: {msg}",
|
||||
"photos": "Только у {real} из {total} народностей Индии есть фото в данных; печатный документ несёт их, плоское изображение — нет. Этот нумерованный дизайн печатает имена без фото, чтобы каждая народность была равной и читаемой.",
|
||||
"posterHeader": "Благословите Индию во имя Иисуса"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "Главный мессенджер (страна)",
|
||||
"messagingNote": "Самый скачиваемый в App Store страны: только iOS, рейтинг загрузок, а не использования.",
|
||||
"province": "Индия и Китай: штат или провинция",
|
||||
"provinceNote": "Приблизительно: по месту проживания народа, а не по городу.",
|
||||
"provinceNote": "Приблизительно: определено по точке группы на карте, а не по городу.",
|
||||
"region": "Регион",
|
||||
"religion": "Религия",
|
||||
"wayIn": "Путь, когда на их языке ничего нет"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "Карта народов, закрашенная по доле пользователей интернета в каждой стране"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "Точка показывает, где этот народ живёт в стране ({country}), а не город.",
|
||||
"pins": "Точка показывает, где народ живёт в своей стране, а не город. Групп, делящих точку с другой группой: {n}.",
|
||||
"pin": "Точка обозначает этот народ, а не город.",
|
||||
"pins": "Точка обозначает народ, а не город. Групп, делящих точку с другой группой: {n}.",
|
||||
"shares": "Делит точку с другими группами: {n}.",
|
||||
"sources": "Источники:",
|
||||
"standing": "Точки приблизительны (где народ живёт в своей стране, а не город). Данные об интернете относятся к стране или индийскому штату, а не к группе. «Найдено» значит найдено в проверенных источниках."
|
||||
"standing": "Точки приблизительны: точка обозначает народ, а не город. Данные об интернете относятся к стране или индийскому штату, а не к группе. «Найдено» значит найдено в проверенных источниках."
|
||||
},
|
||||
"panel": {
|
||||
"charts": "Диаграммы",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "Где живут неохваченные народы",
|
||||
"title": "Скопления меток народов",
|
||||
"bands": {
|
||||
"named": "центр с названием",
|
||||
"multi": "несколько народов",
|
||||
"single": "один народ"
|
||||
},
|
||||
"note": "Точка — это скопление центров народов в радиусе ~25 км, а не граница муниципалитета. Названия точек взяты из цитируемого справочника.",
|
||||
"note": "Точка — это скопление меток народов в радиусе ~25 км, а не граница муниципалитета. Названия точек взяты из цитируемого справочника.",
|
||||
"unnamed": "место без названия — {lat}, {lng}",
|
||||
"close": "Закрыть",
|
||||
"country": "Страна",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "неизвестно",
|
||||
"pgHere": "Народы здесь",
|
||||
"languages": "Языки",
|
||||
"sheetNote": "Скопление центров народов в радиусе ~25 км, а не граница муниципалитета. Названия точек взяты из цитируемого справочника (Nominatim/OSM); у точек без названия показаны координаты, а не выдуманное название.",
|
||||
"sheetNote": "Скопление меток народов в радиусе ~25 км, а не граница муниципалитета. Названия точек взяты из цитируемого справочника (Nominatim/OSM); у точек без названия показаны координаты, а не выдуманное название.",
|
||||
"national": "Данные об интернете — НАЦИОНАЛЬНЫЕ.",
|
||||
"engineMissing": "Не удалось загрузить движок карты."
|
||||
},
|
||||
"list": {
|
||||
"title": "Ключевые города",
|
||||
"intro": "Где живут неохваченные народы: населённых пунктов с названием из цитируемого справочника: {named}, в найденных скоплениях: {clusters}.",
|
||||
"intro": "Скопления меток народов: скоплений с названием из цитируемого справочника: {named}, в найденных скоплениях: {clusters}.",
|
||||
"filters": {
|
||||
"named": "Населённые пункты с названием",
|
||||
"multi": "Скопления нескольких народов",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "народов: {n}",
|
||||
"caveat": {
|
||||
"rowLead": "Что такое строка.",
|
||||
"row": "Скопление центров народов в радиусе ~25 км, а не граница муниципалитета. Точки показывают, где сосредоточен народ, а не поселение, поэтому скопления никогда не называются по догадке: {n} центров с несколькими народами определены через {source} (ODbL, источник в каждой строке), а остальные остаются без названия, а не угаданы.",
|
||||
"row": "Скопление меток народов в радиусе ~25 км, а не граница муниципалитета. Каждая метка обозначает народ, а не город, поэтому скопления никогда не называются по догадке: {n} центров с несколькими народами определены через {source} (ODbL, источник в каждой строке), а остальные остаются без названия, а не угаданы.",
|
||||
"internet": "Они даны как контекст: данные о подключении по городам на этом уровне публично недоступны, и национальный показатель нельзя читать как измерение для города.",
|
||||
"scope": "На этой странице только местоположения народов и данные о подключении — намеренно никаких данных о церквях, верующих или местах встреч."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "Точка показывает, где живёт этот народ, а не город и не границу.",
|
||||
"sharedPoint": "Она совпадает с точкой {names} — {n} групп делят эту координату, поэтому положение указывает местность, а не где какая группа."
|
||||
"approximate": "Точка обозначает народ, а не город и не границу.",
|
||||
"sharedPoint": "Она совпадает с точкой {names} — {n} групп делят эту точку."
|
||||
}
|
||||
|
|
|
|||
15
library/i18n/locales/zh/blessIndiaPosters.json
Normal file
15
library/i18n/locales/zh/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"title": "祝福印度海报",
|
||||
"rangeBadge60x96": "编号标记 · 60×96(5×8 英尺)",
|
||||
"count": "{count} 个族群",
|
||||
"desktopOnly": "目前仅限桌面——尚未证明这张大海报能在手机上安全打印。",
|
||||
"experimental": "实验性",
|
||||
"intro": "印度的每个族群都在一张可打印的大纸上,由实时数据生成,并像其可打印的分页那样带着拼接胶条显示——就像 Poster Design 的尺寸视图展示大幅面那样。",
|
||||
"print": "打印此海报",
|
||||
"attempt": "编号标记 · 60×96(5×8 英尺)· 3 张",
|
||||
"stat": "{groups} 个族群 · {badges} 个编号标记 · 0 重叠",
|
||||
"generating": "正在根据实时数据生成海报……",
|
||||
"error": "无法生成海报:{msg}",
|
||||
"photos": "数据中 {total} 个印度族群中只有 {real} 个有照片;打印文档能承载它们,而平面图像不能。此编号设计打印名称而不带照片,使每个族群平等且清晰易读。",
|
||||
"posterHeader": "奉耶稣的名祝福印度"
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
"messaging": "最主要的通讯应用(国家)",
|
||||
"messagingNote": "该国苹果 App Store 中下载最多的应用:仅限 iOS,是下载排名,不是使用情况。",
|
||||
"province": "印度和中国:邦或省",
|
||||
"provinceNote": "大约:按族群所在地放置,不是城镇。",
|
||||
"provinceNote": "大约:按族群在地图上的圆点确定,不是按城镇。",
|
||||
"region": "地区",
|
||||
"religion": "宗教",
|
||||
"wayIn": "母语一无所有时的进入途径"
|
||||
|
|
@ -200,11 +200,11 @@
|
|||
"aria": "族群地图,按各国互联网使用程度着色"
|
||||
},
|
||||
"notice": {
|
||||
"pin": "圆点标示该民族在{country}的所在地区,而不是某个城镇。",
|
||||
"pins": "圆点标示一个民族在其国家的所在地区,而不是某个城镇。有 {n} 个族群与其他族群共用一个点。",
|
||||
"pin": "圆点代表该民族,而不是某个城镇。",
|
||||
"pins": "圆点代表一个民族,而不是某个城镇。有 {n} 个族群与其他族群共用一个点。",
|
||||
"shares": "它与另外 {n} 个族群共用这个点。",
|
||||
"sources": "来源:",
|
||||
"standing": "圆点是大约位置(民族在其国家的所在地区,不是城镇)。互联网数字是国家或印度某邦的,不是族群的。“找到”是指在已查阅的来源中找到。"
|
||||
"standing": "圆点是大约位置:一个圆点代表一个民族,而不是城镇。互联网数字是国家或印度某邦的,不是族群的。“找到”是指在已查阅的来源中找到。"
|
||||
},
|
||||
"panel": {
|
||||
"charts": "图表",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"map": {
|
||||
"title": "未得之民生活在哪里",
|
||||
"title": "族群标记点的聚合",
|
||||
"bands": {
|
||||
"named": "已命名中心",
|
||||
"multi": "多个族群",
|
||||
"single": "单一族群"
|
||||
},
|
||||
"note": "一个点是约 25 公里内族群中心点的聚合,并非市级边界。已命名的点来自有出处的地名录。",
|
||||
"note": "一个点是约 25 公里内族群标记点的聚合,并非市级边界。已命名的点来自有出处的地名录。",
|
||||
"unnamed": "未命名地点 — {lat},{lng}",
|
||||
"close": "关闭",
|
||||
"country": "国家",
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"unknown": "未知",
|
||||
"pgHere": "此处的族群",
|
||||
"languages": "语言",
|
||||
"sheetNote": "约 25 公里内族群中心点的聚合,并非市级边界。已命名的点来自有出处的地名录(Nominatim/OSM);未命名的点显示坐标,而不是编造的地名。",
|
||||
"sheetNote": "约 25 公里内族群标记点的聚合,并非市级边界。已命名的点来自有出处的地名录(Nominatim/OSM);未命名的点显示坐标,而不是编造的地名。",
|
||||
"national": "互联网数据为全国数据。",
|
||||
"engineMissing": "无法加载地图引擎。"
|
||||
},
|
||||
"list": {
|
||||
"title": "重点城市",
|
||||
"intro": "未得之民生活在哪里:来自有出处地名录的已命名人口中心:{named},分布于已定位聚合:{clusters}。",
|
||||
"intro": "族群标记点的聚合:已命名的聚合(名称来自有出处的地名录):{named},分布于已定位聚合:{clusters}。",
|
||||
"filters": {
|
||||
"named": "已命名的人口中心",
|
||||
"multi": "多族群聚合",
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"peopleGroups": "族群:{n}",
|
||||
"caveat": {
|
||||
"rowLead": "每一行代表什么。",
|
||||
"row": "约 25 公里内族群中心点的聚合,并非市级边界。点标示的是族群的中心位置,而非居民点,因此聚合从不靠推断命名:{n} 个多族群中心通过 {source}(ODbL,每行注明出处)确定,其余的保持未命名,而不是猜测。",
|
||||
"row": "约 25 公里内族群标记点的聚合,并非市级边界。每个标记点代表一个族群,而不是城镇,因此聚合从不靠推断命名:{n} 个多族群中心通过 {source}(ODbL,每行注明出处)确定,其余的保持未命名,而不是猜测。",
|
||||
"internet": "仅作背景参考:在此尺度上没有公开的城市级联网数据,全国数据不能当作城市的测量值。",
|
||||
"scope": "本页只包含族群位置和联网情况——刻意不含任何教会、信徒或聚会地点的数据。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"approximate": "此点标示该民族所在的地区,而非某个城镇或边界。",
|
||||
"sharedPoint": "它与 {names} 位于同一点 — {n} 个群体共用此坐标,因此位置只表示所在地区,而非各群体的具体位置。"
|
||||
"approximate": "此点代表一个民族,而非某个城镇或边界。",
|
||||
"sharedPoint": "它与 {names} 位于同一点 — {n} 个群体共用这个点。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -699,7 +699,7 @@ function drawSeg(){
|
|||
const target = isMobile() ? 'mobile' : 'desktop'
|
||||
const tree = navForTarget(NAV, target)
|
||||
const open = new Map(tree.filter(n=>n.kind==='folder').map(n=>[n.id, !!n.open]))
|
||||
// The profile shown on first load (design note: the prayer map is the first map they see).
|
||||
// The profile shown on first load (the prayer map is the first map a reader sees).
|
||||
// Declared HERE, above its first use — a `const` further down the module would be in
|
||||
// the temporal dead zone at this point and throw.
|
||||
const DEFAULT_PROFILE = 'prayer-progress'
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ const records = await loadRecords();
|
|||
// Run FORMAT=3x4 separately for the oversize tier.
|
||||
// ── CITY MODE (CITIES=1) ─────────────────────────────────────────────────────
|
||||
// One sheet per NAMED POPULATION CENTRE — the Key City Posters app.
|
||||
// A "city" here is a cluster of people-group centroids within ~25km that holds
|
||||
// A "city" here is a cluster of people-group pins within ~25km that holds
|
||||
// MORE THAN ONE group, named by a CITED gazetteer (Nominatim/OSM). Unnamed and
|
||||
// single-group clusters are excluded: a poster titled "unnamed location" is not a
|
||||
// poster, and naming one by inference would fabricate a placename.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
// No church, believer, meeting-point or fellowship data of any kind. This maps
|
||||
// PUBLISHED RESOURCE AVAILABILITY per language — "does a translation exist" — which
|
||||
// is public bibliographic fact. It must never gain a field describing where
|
||||
// Christians are. (design note boundary, 2026-08-01.)
|
||||
// Christians are. (A standing boundary since 2026-08-01.)
|
||||
//
|
||||
// HONESTY RULE: a language absent from eBible is recorded `has_scripture:false`
|
||||
// with `checked_pools:['ebible']`. That means "not found in the pools we checked",
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ if (has('city-candidates.json')) {
|
|||
], {
|
||||
title: 'Key Cities / Location Clusters',
|
||||
grain: 'one row per ~25km location cluster',
|
||||
caveat: 'A cluster is people-group CENTROIDS, not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.',
|
||||
caveat: 'A cluster is people-group PINS (each marks a people group, not a town), not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.',
|
||||
internet_caveat: 'Internet figures are NATIONAL. Per-city connectivity is not publicly available at this scale.',
|
||||
}, (r) => {
|
||||
const namedRows = r.filter((x) => x.named);
|
||||
|
|
@ -729,7 +729,7 @@ if (has('city-candidates.json') && has('language-resources.json')) {
|
|||
title: 'Languages per City x Resources',
|
||||
grain: 'one row per location cluster x language spoken there',
|
||||
why: '399 of 1,646 clusters are multi-language — a city-level bridge can exist that a country average hides.',
|
||||
place_caveat: 'A location is a cluster of people-group centroids within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.',
|
||||
place_caveat: 'A location is a cluster of people-group pins within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.',
|
||||
resolution_caveat: 'resolved:false means the language LABEL did not match an ISO 639-3 record here — its resource status is UNKNOWN, not absent.',
|
||||
pools_caveat: 'Resource status is from eBible + JESUS Film only. GRN and FCBH audio are key-blocked.',
|
||||
}, (r) => {
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ for (const [key, members] of cells) {
|
|||
provenance: {
|
||||
derived_from: 'doxa people-group records (lat/lng centroids)',
|
||||
method: `~25km cell aggregation (quarter-degree), cell=${key}`,
|
||||
caveat: 'NOT a settlement. A cluster of people-group centroids; place_name is unresolved.',
|
||||
caveat: 'NOT a settlement. A cluster of people-group pins; place_name is unresolved.',
|
||||
derived_on: '2026-08-01',
|
||||
},
|
||||
});
|
||||
|
|
@ -122,7 +122,7 @@ const stats = {
|
|||
internet_researched: clusters.filter((c) => c.internet_access !== 'unknown').length,
|
||||
generated: '2026-08-01',
|
||||
scope_note: 'Scope is the full 2,106-record DX people-group corpus. No separate "2000 list" artefact exists on disk — verified by search — so this corpus IS the scope.',
|
||||
safety_note: 'Contains NO gospel-resource, church or believer field, and must not gain one. That join is held pending design note architecture (separate store, never joined to UPG locations in a public artifact).',
|
||||
safety_note: 'Contains NO gospel-resource, church or believer field, and must not gain one. That join is held pending its own architecture (separate store, never joined to UPG locations in a public artifact).',
|
||||
};
|
||||
|
||||
if (!existsSync(OUT)) mkdirSync(OUT, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// ALL=1 node name-clusters.mjs # all 1,646 (slow: ~28 min at 1 req/s)
|
||||
//
|
||||
// ═══ WHY A GAZETTEER AND NOT CLUSTERING ═════════════════════════════════════
|
||||
// derive-city-candidates.mjs deliberately emits place_name:null. Pins are
|
||||
// people-group CENTROIDS, not settlements, so inferring a placename from pin
|
||||
// derive-city-candidates.mjs deliberately emits place_name:null. Each pin marks
|
||||
// a people group, not a settlement, so inferring a placename from pin
|
||||
// geometry would MANUFACTURE it — and a fabricated placename in a
|
||||
// persecution-sensitive dataset is the dangerous kind of wrong. Naming must come
|
||||
// from a source that can be cited per row. This is that step.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Key Cities / Location Clusters",
|
||||
"grain": "one row per ~25km location cluster",
|
||||
"caveat": "A cluster is people-group CENTROIDS, not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"caveat": "A cluster is people-group PINS (each marks a people group, not a town), not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"internet_caveat": "Internet figures are NATIONAL. Per-city connectivity is not publicly available at this scale."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "Languages per City x Resources",
|
||||
"grain": "one row per location cluster x language spoken there",
|
||||
"why": "399 of 1,646 clusters are multi-language — a city-level bridge can exist that a country average hides.",
|
||||
"place_caveat": "A location is a cluster of people-group centroids within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"place_caveat": "A location is a cluster of people-group pins within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"resolution_caveat": "resolved:false means the language LABEL did not match an ISO 639-3 record here — its resource status is UNKNOWN, not absent.",
|
||||
"pools_caveat": "Resource status is from eBible + JESUS Film only. GRN and FCBH audio are key-blocked."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
// Root cause: a one-element dfrac sweep + leaders still drawn beside
|
||||
// the numbered badges + a DENSE_AT threshold that exists only as a
|
||||
// comment. Fixing it changes the look of 41 posters in the SHARED
|
||||
// template, which is a design note design call.
|
||||
// template, which is a product design call.
|
||||
// ALSO expect ONE "no crossing count" failure for country-chn: it is
|
||||
// the 3x4 tier, built by a SEPARATE run, so the 173-country manifest
|
||||
// does not cover it. That is the guard refusing to call an unmeasured
|
||||
|
|
|
|||
13
package.json
13
package.json
|
|
@ -225,7 +225,6 @@
|
|||
"check:selector-fields:self-test": "node internal/check-selector-fields.mjs --self-test",
|
||||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
|
||||
"check:packet-variants": "node internal/check-packet-variants.mjs",
|
||||
"check:circular-layout": "node library/packet/scripts/check-circular-layout.mjs",
|
||||
"check:fork-leaders": "node library/packet/scripts/check-fork-leaders.mjs",
|
||||
"check:pins-in-state": "node library/packet/scripts/check-pins-in-state.mjs",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
|
|
@ -356,8 +355,8 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:circular-layout check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test check:phone-print-keep:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -434,9 +433,12 @@
|
|||
"check:poster-type-registry": "node internal/check-poster-type-registry.mjs",
|
||||
"check:poster-type-registry:self-test": "node internal/check-poster-type-registry.mjs --self-test",
|
||||
"check:host-floor": "node internal/check-host-floor.mjs",
|
||||
"check:host-floor:self-test": "node internal/check-host-floor.mjs --self-test"
|
||||
"check:host-floor:self-test": "node internal/check-host-floor.mjs --self-test",
|
||||
"check:phone-print-keep": "node internal/check-phone-print-keep.mjs",
|
||||
"check:phone-print-keep:self-test": "node internal/check-phone-print-keep.mjs --self-test"
|
||||
},
|
||||
"//scripts": {
|
||||
"check:phone-print-keep": "on a PHONE the in-page print document (#dxm-print-root and its print style, library/composables/printInPlace.js) and printImageBlob's object URL outlive afterprint, because Android Chrome fires afterprint once the PREVIEW is built and renders a saved PDF later, from the page as it is then (a phone's saved packet was the bare host page). A timeline on fake timers and a stub DOM with an Android user agent: still mounted 30 s after afterprint; released once the reader is back (shown again after hidden, focused again after blur, or touched after print() but not in its first TOUCH_SETTLE_MS) plus BACK_GRACE_MS (60 s), or at the ceiling; going away again during the grace cancels the pending drop; the Print tap of a late print does not count; a new print replaces it; desktop is the frame path, verbatim. --impl=<path> runs it against another copy (the pre-fix file fails). Self-test poisons: the old drop at afterprint, no release when back, listeners armed at mount, no grace, printImageBlob revoking at afterprint, no ceiling, no cancel when away again, no touch settle window.",
|
||||
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
|
||||
"browser-gates": "lists every package.json script that launches a browser (the same detection run-parallel uses to route them through internal/browser-slot.sh). Run each as internal/browser-slot.sh bun run <name>.",
|
||||
"cap:sync": "NOT `bunx` — bunx is not on PATH in this environment (command not found, exit 127), which broke cap:sync -> apk:debug -> phone at step one. The CLI IS installed locally (@capacitor/cli 7.6.9); invoke the local bin directly. The rest of package.json already uses the spaced `bun x` form.",
|
||||
|
|
@ -587,7 +589,6 @@
|
|||
"check:packet-variants": "the poster wall's Prayer List prints the PACKET (not the columnar list), and every variant it offers exists in packet-render's VARIANTS. A variant may drop fixed pages but never people: lightweight keeps every group. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
"check:pins-in-state": "library/packet/pins-in-state.js places whole-India poster pins so no two overlap AND every pin stays inside its own ADM1 state polygon, at the 60x96in sheet scale (2500x4000px, PIN_R 8). A deterministic repulsion relaxation (imitating poster-render.js, read not called) with the map-rect clamp replaced by a per-state multipolygon clamp: a pin pushed off its land is pulled to the nearest point just inside, holes and multi-part states (islands/coasts) honoured. Runs on the real India 855-pin data when present, else a synthetic 60x96 fixture (floor 200 pins, no vacuous pass); asserts 0 pins outside their state and 0 overlaps that are not NAMED in `infeasible` (a too-small state is reported, never silently stacked), plus a hole/island arm and a poison arm. Node only, headless, deterministic.",
|
||||
"check:fork-leaders": "library/packet/fork-leaders.js routes ONE paired-row leader as an octilinear fork: a stem forks at a 45° chamfer into two short H/V legs, one per pin. Modes row and trunk. Returns polylines + a deduped segs list (one polyline id per fork for crossings). Reports octilinear + throughPins. Pure, px-only, deterministic. The gate asserts both modes octilinear, legs land on pins, 0 crossings on a 220-row set (floor 200), through-pin hit+miss, determinism, and a poison. Node only, headless.",
|
||||
"check:circular-layout": "library/packet/circularLayout.js is the poster circular-logic (ring order + leader routing + numbered four-sides + paired-row forks) as one pure Worker-safe module: numbers in, geometry out (positions/polylines/stats), no SVG. Ported from poster-render.js + the classic4 four-sides reference. Modes sideLanes (today L/R), fourSides (design 1: numbered, no leaders, 0 crossings), pairedRows (design 2: two leaders/row, 0 crossings). forkRoute + placePinsInState are static imports (opts may override for a Worker). Gate asserts on a frozen 855-pin 60x96 fixture: fourSides ring-ordered + 0 dupes + 0 pins-outside-state (floor 800) + side sequence L<T<R<B + geometric monotonicity per side, pairedRows 0 crossings + octilinear + every leader polys.length===2, sideLanes runs octilinear, determinism, a poison, output recounts, and a node time/heap budget. Deterministic; ties by code unit. Node only, headless.",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||||
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
||||
|
|
@ -618,7 +619,7 @@
|
|||
"check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.",
|
||||
"check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
|
||||
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
|
||||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters, bless-india-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||||
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
|
||||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (upg-neighbor's presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||||
"check:profile-table": "the prayer packet's profile TABLE (20 full-width rows: picture, generated description, three boxes; PER_PAGE stays 20): a row stays tall enough for a name plus a two-line description at the smallest fit size, and truncation happens only at a whole word. Character-count, not DOM, so Node and the browser agree. Cannot see the RENDERED row height: that is check:packet-render, a browser gate. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
|
|
|
|||
25
plugin-designs/bless-india-posters/README.md
Normal file
25
plugin-designs/bless-india-posters/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# bless-india-posters — whole-India poster attempts (generative, experimental)
|
||||
|
||||
A preview surface for the hardest poster we make: every one of India's ~855 people groups on one
|
||||
printable sheet. Each design attempt is **generated live** from the current people-group data — nothing
|
||||
is stored — and shown the way Poster Design's Sizes view shows the big formats: the sheet split into its
|
||||
printable sheets with the glue strips between them.
|
||||
|
||||
Every poster carries the blessing: the header **"Bless India in Jesus' name"** and, as the footer, the
|
||||
Aaronic blessing (Numbers 6:24-26, public-domain World English Bible, British Edition — switchable).
|
||||
|
||||
- **Public + experimental.** The attempts ship in the public build for this plugin; the whole-country
|
||||
poster on Country Posters stays withheld (not shipped) until it prints legibly.
|
||||
- **Phone-safe by default.** A phone sees the preview, but Print is blocked with a "desktop only" message
|
||||
until a design is proven to generate and print on a phone without crashing.
|
||||
- **Prints as a document.** A flat image can't carry people-group photos (the photo hosts send no CORS),
|
||||
and a 60×96 sheet rasterised onto letter would be shrunk — so Print opens the poster as its own document
|
||||
at its true size.
|
||||
|
||||
## Files
|
||||
- `index.js` — registers `<bless-india-posters>` and the `blessIndia` poster type (for the catalogue).
|
||||
- `bless-india-posters.vue` — the view: generate live → the W via the shared library `PanelSheets` + print.
|
||||
- `src/generate.js` — records + boundaries + format → the finished poster SVG/HTML (pure wiring).
|
||||
- `src/generators/` — the plugin's own copy of the renderer (a stopgap; attempts move onto the shared
|
||||
`@map` circular-layout module when it lands, and this copy is deleted).
|
||||
- `src/blessIndiaPosterType.js` — the poster-type adapter. `src/boundaries.js` — the India boundary fetch.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"label": "Bless India Posters",
|
||||
"stage": "experimental",
|
||||
"visibility": "team",
|
||||
"posterDesign": [
|
||||
{
|
||||
"family": "blessPoster",
|
||||
"name": "bless-india.range-badge-60x96",
|
||||
"size": "60x96",
|
||||
"mobileFriendly": false,
|
||||
"labelKey": "blessIndiaPosters.rangeBadge60x96"
|
||||
}
|
||||
],
|
||||
"_comment": "Experimental plugin: our whole-India poster attempts, generated live (no stored rasters). The attempts ship in the PUBLIC build for this plugin; the whole-country poster on Country Posters stays withheld. posterDesign registers each design in the Poster Design catalogue; names are ledgered and never renamed once shipped. mobileFriendly stays false until a design is proven to generate + print on a phone without crashing (the desktop-only rule shows meanwhile). More designs (four-sides 5x8, paired-rows 5.5x5.5) land with later attempts."
|
||||
}
|
||||
179
plugin-designs/bless-india-posters/bless-india-posters.vue
Normal file
179
plugin-designs/bless-india-posters/bless-india-posters.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<!--
|
||||
bless-india-posters.vue — the <bless-india-posters> element's root (PUBLIC, experimental).
|
||||
|
||||
@element bless-india-posters
|
||||
@description A preview-only review board for the whole-India poster problem — fitting every one of
|
||||
India's people groups onto a single printable sheet. It generates each design attempt live from the
|
||||
current data (nothing stored) and shows it the way Poster Design's Sizes view does: the big sheet
|
||||
split into its printable sheets with the glue strips between them. The blessing — "Bless India in
|
||||
Jesus' name" and the Aaronic blessing (Numbers 6:24-26) — is printed on every poster. Experimental;
|
||||
a phone sees the preview but Print is blocked until a design is proven safe on a phone.
|
||||
|
||||
Generates with the plugin's own stopgap generator (src/generators/, a copy of the renderer — no edits
|
||||
to the shared poster-render.js); attempts move onto the shared @map circularLayout module when it
|
||||
lands and the stopgap is deleted. Shows the W via the shared library PanelSheets (@map); prints the
|
||||
poster as its own document (a raster can't carry people-group photos, and a 60x96 sheet rasterised
|
||||
onto letter would be shrunk).
|
||||
-->
|
||||
<template>
|
||||
<div class="bip-root" :dir="dir">
|
||||
<header class="bip-top">
|
||||
<div>
|
||||
<h1>{{ t('title') }} <span class="bip-exp">{{ t('experimental') }}</span></h1>
|
||||
<p class="bip-sub">{{ t('intro') }}</p>
|
||||
</div>
|
||||
<div class="bip-actions">
|
||||
<button v-if="!isPhone" class="bip-btn" :disabled="!posterHtml || busy" @click="printPoster">
|
||||
{{ t('print') }}
|
||||
</button>
|
||||
<span v-else class="bip-phone">{{ t('desktopOnly') }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="bip-attempt">
|
||||
<div class="bip-meta">
|
||||
<span class="bip-name">{{ t('attempt') }}</span>
|
||||
<span class="bip-stat" v-if="stats">{{ t('stat', { groups: n(stats.placed), badges: n(stats.rangeBadges) }) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="bip-stage">
|
||||
<div v-if="busy" class="bip-note">{{ t('generating') }}</div>
|
||||
<div v-else-if="error" class="bip-note bip-err">{{ t('error', { msg: error }) }}</div>
|
||||
<PanelSheets
|
||||
v-else-if="svgUrl"
|
||||
v-bind="SHEETS"
|
||||
join-fill="bleed"
|
||||
:gap-in="0.75"
|
||||
:src="svgUrl"
|
||||
class="bip-panels"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="bip-photos">{{ t('photos', { real: n(realPhotos), total: n(stats ? stats.placed : 855) }) }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* bless-india-posters.vue — the <bless-india-posters> element.
|
||||
*
|
||||
* @element bless-india-posters
|
||||
* @description A preview surface for the whole-India poster problem — fitting every one of India's people groups onto a single printable sheet. Each design attempt is generated live from the current data (nothing stored) and shown as its printable sheets with the glue strips, headed "Bless India in Jesus' name" with the Numbers 6:24-26 blessing. Experimental; a phone sees the preview but Print is blocked until a design is proven safe on a phone.
|
||||
*/
|
||||
import { ref, shallowRef, onMounted, onBeforeUnmount } from 'vue'
|
||||
import PanelSheets from '@map/components/poster/PanelSheets.vue'
|
||||
import { POSTER_LADDER } from '@map/constants/posterSizes.js'
|
||||
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||||
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
import { ISO3_NAME } from '@map/data/iso3-name.js'
|
||||
import seriesJson from '@map/scripts/posters/series.json'
|
||||
import { generatePoster, BLESSING } from './src/generate.js'
|
||||
import { loadBoundaries } from './src/boundaries.js'
|
||||
|
||||
// The host (a shell, or a plain embed) passes profile-config; the shells (Posterverse, the Research
|
||||
// Gallery) write an ABSOLUTE assetsBase + treeBase into it. resolveAssetsBase reads that, else falls
|
||||
// to the bundle's own tree root (import.meta.url, captured at script load — NOT a runtime
|
||||
// deriveTreeBase(), which returns '' once document.currentScript is gone, the bug that made the
|
||||
// boundaries load from /assets, the host's HTML page). Rule: load assets from the assets base.
|
||||
const props = defineProps({ profileConfig: { type: [String, Object], default: '' } })
|
||||
const parsedConfig = (() => {
|
||||
const c = props.profileConfig
|
||||
if (!c) return {}
|
||||
if (typeof c === 'object') return c
|
||||
try { return JSON.parse(c) } catch { return {} }
|
||||
})()
|
||||
|
||||
// Lightweight i18n for the plugin's OWN page chrome, so Arabic readers read Arabic (not English with
|
||||
// misplaced RTL punctuation). Reads the locale the shells pass (profileConfig.locale/lang), loads the
|
||||
// blessIndiaPosters.json for that language (English fallback), and sets dir for RTL scripts. Counts go
|
||||
// through the locale's number format. The POSTER's blessing header/footer is the separate footer work.
|
||||
const LOCALE_MSGS = import.meta.glob('../../library/i18n/locales/*/blessIndiaPosters.json', { eager: true })
|
||||
const msgPath = (l) => `../../library/i18n/locales/${l}/blessIndiaPosters.json`
|
||||
const urlLang = (() => { try { return new URLSearchParams(location.search).get('lang') || '' } catch { return '' } })()
|
||||
const docLang = (() => { try { return document.documentElement.lang || '' } catch { return '' } })()
|
||||
const lang = String(parsedConfig.locale || parsedConfig.lang || urlLang || docLang || 'en').split('-')[0].toLowerCase()
|
||||
const EN = (LOCALE_MSGS[msgPath('en')] || {}).default || {}
|
||||
const MSGS = (LOCALE_MSGS[msgPath(lang)] || {}).default || EN
|
||||
const dir = ['ar', 'he', 'fa', 'ur', 'ps', 'sd', 'yi'].includes(lang) ? 'rtl' : 'ltr'
|
||||
const nf = (() => { try { return new Intl.NumberFormat(lang) } catch { return new Intl.NumberFormat('en') } })()
|
||||
const n = (x) => nf.format(Number(x) || 0)
|
||||
const t = (key, vars) => {
|
||||
let s = (MSGS[key] != null ? MSGS[key] : EN[key] != null ? EN[key] : key)
|
||||
if (vars) for (const [k, v] of Object.entries(vars)) s = String(s).split(`{${k}}`).join(v)
|
||||
return s
|
||||
}
|
||||
|
||||
const SHEETS = POSTER_LADDER['60x96'].sheets // { sheetW:60, sheetH:36, count:3, direction, joinIn:6 }
|
||||
const seriesDef = (seriesJson && seriesJson.country) || {}
|
||||
|
||||
const isPhone = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(max-width: 767px)').matches
|
||||
|
||||
const busy = ref(true)
|
||||
const error = ref('')
|
||||
const svgUrl = shallowRef('')
|
||||
const posterHtml = shallowRef('')
|
||||
const stats = ref(null)
|
||||
const realPhotos = ref(151)
|
||||
let blobUrl = ''
|
||||
|
||||
async function build() {
|
||||
busy.value = true; error.value = ''
|
||||
try {
|
||||
const recordsFor = createPeopleGroupRecords({ apiBase: () => getApiBaseUrl(), lang: () => 'en' })
|
||||
const [records, boundaries] = await Promise.all([
|
||||
recordsFor({ type: 'country', values: ['IND'] }),
|
||||
loadBoundaries(resolveAssetsBase(parsedConfig)),
|
||||
])
|
||||
const { html, svg, stats: s, count } = await generatePoster({
|
||||
records, ...boundaries, seriesDef, iso3Name: ISO3_NAME, format: '60x96c4b',
|
||||
// The poster's own header is a phrase (not scripture), so localize it to the reader's language;
|
||||
// the footer verse stays the English PD blessing until the per-locale strict-PD sourcing lands.
|
||||
blessing: { ...BLESSING, header: t('posterHeader') },
|
||||
})
|
||||
posterHtml.value = html
|
||||
stats.value = s
|
||||
realPhotos.value = records.filter((r) => typeof r.image_url === 'string' && !/no-photo/.test(r.image_url)).length || 151
|
||||
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||
blobUrl = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }))
|
||||
svgUrl.value = blobUrl
|
||||
} catch (e) {
|
||||
error.value = (e && e.message) || String(e)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function printPoster() {
|
||||
if (!posterHtml.value) return
|
||||
const w = window.open('', '_blank')
|
||||
if (!w) return
|
||||
w.document.open(); w.document.write(posterHtml.value); w.document.close()
|
||||
const go = () => { try { w.focus(); w.print() } catch {} }
|
||||
if (w.document.readyState === 'complete') setTimeout(go, 300); else w.onload = () => setTimeout(go, 300)
|
||||
}
|
||||
|
||||
onMounted(build)
|
||||
onBeforeUnmount(() => { if (blobUrl) URL.revokeObjectURL(blobUrl) })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bip-root { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #1c1c17; background: #f7f4ec; padding: 20px; box-sizing: border-box; min-height: 100%; }
|
||||
.bip-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; border-bottom: 2px solid #d9d2c2; padding-bottom: 14px; margin-bottom: 18px; }
|
||||
.bip-top h1 { font-size: 22px; margin: 0 0 4px; display: flex; align-items: center; gap: 10px; }
|
||||
.bip-exp { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: #b23b2e; border: 1px solid #b23b2e; border-radius: 999px; padding: 2px 8px; }
|
||||
.bip-sub { margin: 0; max-width: 64ch; font-size: 13px; color: #55503f; }
|
||||
.bip-actions { display: flex; align-items: center; }
|
||||
.bip-btn { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 9px 14px; font-size: 13px; cursor: pointer; }
|
||||
.bip-btn:disabled { opacity: .5; cursor: default; }
|
||||
.bip-phone { font-size: 12.5px; color: #b23b2e; max-width: 24ch; text-align: right; }
|
||||
.bip-attempt { max-width: 820px; margin: 0 auto; }
|
||||
.bip-meta { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 12.5px; color: #55503f; margin-bottom: 8px; }
|
||||
.bip-name { font-weight: 700; color: #1c1c17; }
|
||||
.bip-stage { background: #efe9db; border: 1px solid #e4ddcc; border-radius: 10px; padding: 16px; min-height: 240px; display: flex; align-items: center; justify-content: center; }
|
||||
.bip-panels { width: 100%; max-width: 560px; margin: 0 auto; }
|
||||
.bip-note { color: #55503f; font-size: 14px; }
|
||||
.bip-err { color: #b23b2e; }
|
||||
.bip-photos { font-size: 12px; color: #6b6450; margin-top: 10px; max-width: 70ch; }
|
||||
</style>
|
||||
20
plugin-designs/bless-india-posters/index.html
Normal file
20
plugin-designs/bless-india-posters/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doxa-maps · India Posters staging</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { background: #f7f4ec; min-height: 100%; font-family: system-ui, sans-serif; }
|
||||
bless-india-posters { display: block; width: 100%; min-height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- In dev (the pilot) the preview flag is on and the attempts render; a production build shows
|
||||
only the withheld sentence. The element registers itself from ./index.js. -->
|
||||
<bless-india-posters></bless-india-posters>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
plugin-designs/bless-india-posters/index.js
Normal file
22
plugin-designs/bless-india-posters/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* bless-india-posters / index.js — registers <bless-india-posters> (public, experimental).
|
||||
*
|
||||
* Shows our whole-India poster attempts, generated live (no stored rasters). The attempts ship in the
|
||||
* PUBLIC build for this plugin (the whole-country poster on Country Posters stays withheld; India
|
||||
* tab is not shipped). Also registers the 'blessIndia' poster type so the standard poster section and the
|
||||
* Poster Design catalogue can list + offer the same designs.
|
||||
*/
|
||||
import { defineCustomElement } from 'vue'
|
||||
import BlessIndiaPosters from './bless-india-posters.vue'
|
||||
import { registerPosterType } from '@map/components/poster/posterTypes.js'
|
||||
import blessIndiaType from './src/blessIndiaPosterType.js'
|
||||
|
||||
registerPosterType('blessIndia', blessIndiaType)
|
||||
|
||||
const BlessIndiaPostersElement = defineCustomElement(BlessIndiaPosters)
|
||||
|
||||
if (typeof customElements !== 'undefined' && !customElements.get('bless-india-posters')) {
|
||||
customElements.define('bless-india-posters', BlessIndiaPostersElement)
|
||||
}
|
||||
|
||||
export default BlessIndiaPostersElement
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* blessIndiaPosterType.js — the 'blessIndia' poster type (adapter) for <dxm-poster-section> + the
|
||||
* Poster Design catalogue. The attempts are our whole-India poster designs; each generates live from
|
||||
* the India people-group records (nothing stored). sheet(card, format) returns the poster HTML. The
|
||||
* plugin's own page (bless-india-posters.vue) shows the W via PanelSheets and prints as a document;
|
||||
* this adapter lets the standard poster section/catalogue list and offer the same designs.
|
||||
*/
|
||||
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||||
import { ISO3_NAME } from '@map/data/iso3-name.js'
|
||||
import seriesJson from '@map/scripts/posters/series.json'
|
||||
import { generatePoster } from './generate.js'
|
||||
import { loadBoundaries } from './boundaries.js'
|
||||
|
||||
const seriesDef = (seriesJson && seriesJson.country) || {}
|
||||
const GEN = { '60x96': '60x96c4b', '66x66': '66x66c4b' }
|
||||
const ATTEMPTS = [{ id: 'range-badge-60x96', iso3: 'IND', size: '60x96', label: 'Bless India — range badges (60×96)' }]
|
||||
const isPhone = () => typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia('(max-width: 767px)').matches
|
||||
|
||||
export default {
|
||||
items() { return ATTEMPTS },
|
||||
title(scope, t) { return (t && t('blessIndiaPosters.title')) || 'Bless India Posters' },
|
||||
actions({ assetsBase, apiBase, lang, profileConfig, t }) {
|
||||
const recordsFor = createPeopleGroupRecords({ apiBase, lang, profileConfig })
|
||||
const sheet = async (card, format) => {
|
||||
const genFormat = GEN[format] || GEN['60x96']
|
||||
const records = await recordsFor({ type: 'country', values: ['IND'] })
|
||||
const b = await loadBoundaries(assetsBase)
|
||||
const { html } = await generatePoster({ records, ...b, seriesDef, iso3Name: ISO3_NAME, format: genFormat })
|
||||
return html
|
||||
}
|
||||
return {
|
||||
records: () => recordsFor({ type: 'country', values: ['IND'] }),
|
||||
sheet,
|
||||
unsupported: () => (isPhone() ? ((t && t('blessIndiaPosters.desktopOnly')) || 'Desktop only for now.') : null),
|
||||
unit: 'UPG', itemCountKey: 'blessIndiaPosters.count', eventName: 'bless-india:show-on-map', downloadPrefix: 'bless-india', cardStocks: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
22
plugin-designs/bless-india-posters/src/boundaries.js
Normal file
22
plugin-designs/bless-india-posters/src/boundaries.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* boundaries.js — fetch the India boundary geojson the generator draws on, from the served tree.
|
||||
* Cached per base, so re-generating (a size switch, a re-render) costs one fetch. The generator only
|
||||
* needs India's own ADM0 feature + its ADM1 states + place labels; we fetch the tree files and the
|
||||
* generator filters India out of ADM0 itself.
|
||||
*/
|
||||
const cache = new Map()
|
||||
|
||||
export function loadBoundaries(assetsBase) {
|
||||
const base = String((typeof assetsBase === 'function' ? assetsBase() : assetsBase) || './assets').replace(/\/$/, '')
|
||||
if (cache.has(base)) return cache.get(base)
|
||||
const b = `${base}/boundaries`
|
||||
const p = Promise.all([
|
||||
fetch(`${b}/world-adm0.geojson`).then((r) => r.json()),
|
||||
fetch(`${b}/gb-IND-ADM1.geojson`).then((r) => r.json()),
|
||||
fetch(`${b}/ne-places.json`).then((r) => r.json()).catch(() => ({ places: [] })),
|
||||
]).then(([adm0, adm1Ind, ne]) => ({
|
||||
adm0, adm1Ind, nePlaces: Array.isArray(ne) ? ne : (ne.places || []),
|
||||
}))
|
||||
cache.set(base, p)
|
||||
return p
|
||||
}
|
||||
95
plugin-designs/bless-india-posters/src/generate.js
Normal file
95
plugin-designs/bless-india-posters/src/generate.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* generate.js — pure wiring: live people-group records + India boundaries + a format → the finished
|
||||
* poster SVG string, generated at call time (nothing stored). NO fetch and NO DOM here; the caller (the
|
||||
* Vue view in a browser, or a node test) supplies the records and the boundary geojson, so this stays
|
||||
* testable and Worker-friendly. The generator is the LOCAL STOPGAP copy under ./generators/; attempts
|
||||
* move onto the shared @map circular-layout module when it lands and this copy is deleted.
|
||||
*/
|
||||
import { createPosterGenerator } from './generators/poster-render.js'
|
||||
|
||||
const INDIA = {
|
||||
id: 'country-ind', zoneKey: 'country-ind', label: 'India', color: '#d4a640',
|
||||
adm0: ['IND'], selector: { type: 'country_codes', values: ['IND'] },
|
||||
provinces: ['36 states and union territories'],
|
||||
}
|
||||
|
||||
// The blessing on every poster. Header = the banner; footer = the Aaronic blessing (Numbers 6:24-26).
|
||||
// WEB British Edition ("The LORD …"), public domain, switchable: pass a different `footer` to change
|
||||
// the version (e.g. NIV, which needs Biblica's notice). Header mirrors the Wall of People Groups' spirit.
|
||||
export const BLESSING = {
|
||||
header: "Bless India in Jesus' name",
|
||||
footer: {
|
||||
text: 'The LORD bless you, and keep you. The LORD make his face to shine on you, and be gracious to you. The LORD lift up his face toward you, and give you peace.',
|
||||
ref: 'Numbers 6:24-26',
|
||||
},
|
||||
}
|
||||
|
||||
/** A people-group record (PRINT_FIELDS: latitude/longitude/name/…) → the generator's pin shape. */
|
||||
export function recordsToPins(records) {
|
||||
return (records || [])
|
||||
.map((r) => ({
|
||||
name: r.name,
|
||||
lng: r.longitude != null ? +r.longitude : r.lng != null ? +r.lng : null,
|
||||
lat: r.latitude != null ? +r.latitude : r.lat != null ? +r.lat : null,
|
||||
country_code: r.country_code || 'IND',
|
||||
image_url: r.image_url,
|
||||
population: r.population != null ? +r.population : (r.p != null ? +r.p : undefined),
|
||||
wagf_region: r.wagf_region,
|
||||
slug: r.slug,
|
||||
}))
|
||||
.filter((p) => p.name && Number.isFinite(p.lng) && Number.isFinite(p.lat))
|
||||
}
|
||||
|
||||
// Lift the finished <svg> out of the poster HTML WITH its stylesheet inlined. The poster's fills use
|
||||
// CSS vars (--paper/--sea/--frameline) defined in the HTML's <style> :root; without that block every
|
||||
// fill falls back to black. Same approach as @map/packet/poster-raster.js inlineSvg (replicated here,
|
||||
// pure, so the plugin needn't import the raster module). :root inside a standalone <svg> matches the
|
||||
// svg root, so the vars resolve.
|
||||
function inlineSvg(html) {
|
||||
const s = String(html || '')
|
||||
const svg = (s.match(/<svg[\s\S]*?<\/svg>/) || [])[0]
|
||||
if (!svg) return ''
|
||||
const css = (s.match(/<style>([\s\S]*?)<\/style>/) || [])[1] || ''
|
||||
const safe = css.replace(/<\/style>/gi, '')
|
||||
return svg.replace(/<svg([^>]*)>/, `<svg$1><style>${safe}</style>`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ svg:string, stats:object, title:string, count:number }>}
|
||||
* `svg` is the finished poster as one standalone <svg> (feed it to PanelSheets as a data URL).
|
||||
*/
|
||||
export async function generatePoster({
|
||||
records, adm0, adm1Ind, nePlaces = [], seriesDef = {}, iso3Name = {},
|
||||
format = '60x96c4b', env = {}, blessing = BLESSING,
|
||||
}) {
|
||||
const pins = recordsToPins(records)
|
||||
const zone = { ...INDIA, label: blessing.header, count: pins.length }
|
||||
env = { ...env, FOOTER_VERSE_JSON: JSON.stringify(blessing.footer) }
|
||||
// Physical sheet seams as page-height fractions (trim edge of each non-last sheet), so the layout
|
||||
// can nudge text off them. Derived from the format's sheet plan: 60x96 = three 36in sheets, 6in joins
|
||||
// → seams at 36in and 66in of 96in. (Only whole-page formats; small sizes have no seam.)
|
||||
const SEAMS = { '60x96c4b': [0.375, 0.6875], '60x96c4': [0.375, 0.6875] }
|
||||
if (SEAMS[format]) env = { ...env, SEAM_FRACS_JSON: JSON.stringify(SEAMS[format]) }
|
||||
const feats = (adm0 && adm0.features) || []
|
||||
const zoneFC = {
|
||||
type: 'FeatureCollection',
|
||||
features: feats
|
||||
.filter((f) => f.properties && f.properties.iso3 === 'IND')
|
||||
.map((f) => ({
|
||||
type: 'Feature',
|
||||
properties: { ...f.properties, zoneKey: zone.zoneKey, zoneColor: zone.color, zoneLabel: zone.label },
|
||||
geometry: f.geometry,
|
||||
})),
|
||||
}
|
||||
// This plugin's own banner + series words: no "Pray for" qualifier (header is exactly the blessing),
|
||||
// and a series name of its own rather than the country-map template's.
|
||||
const sd = { ...seriesDef, poster: { ...(seriesDef.poster || {}), titleQualifier: '', seriesName: 'Bless India Posters' } }
|
||||
const gen = createPosterGenerator({
|
||||
env: { ...env, FORMAT: format, SPARSE: '1' },
|
||||
SERIES_DEF: sd, ISO3_NAME: iso3Name,
|
||||
adm0, adm1Ind, adm1Chn: null, nePlaces,
|
||||
series: { totalGroups: pins.length, totalLists: 1, index: 1 },
|
||||
})
|
||||
const out = await gen.renderPoster(zone, pins, zoneFC)
|
||||
return { html: out.html, svg: inlineSvg(out.html), stats: out.indiaSheet || null, title: out.title || 'India', count: pins.length }
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* fork-leaders.js — route ONE paired-row leader as an octilinear fork.
|
||||
*
|
||||
* A paired row has two pins (a, b) sharing one lane port on the frame margin.
|
||||
* Instead of two separate leaders, one stem leaves the port and FORKS at a 45°
|
||||
* chamfer into two short legs, one to each pin. This is the per-fork geometry
|
||||
* only; the caller pairs the rows, assigns the lanes and batches every row's
|
||||
* polylines through the engine's crossing counter.
|
||||
*
|
||||
* It imitates poster-render.js's octiRoute: a trace built from horizontal,
|
||||
* vertical and exact-45° segments, STUB 7 off the port, 45° chamfer corners, no
|
||||
* curves. The output is POLYLINES ([[x,y],…]) so it drops straight into
|
||||
* countPolyCrossings (which skips same-polyline pairs by index).
|
||||
*
|
||||
* MODES (c1/c2):
|
||||
* 'row' — fork AT the row end: the stem is ~0, both legs leave the lane port
|
||||
* directly to a and b. No long trunk across the page.
|
||||
* 'trunk' — the stem runs `dirOut` onto the page toward the pair's midpoint
|
||||
* (by forkFrac of the port→midpoint horizontal budget), THEN forks
|
||||
* near the pins.
|
||||
*
|
||||
* PX ONLY. DETERMINISTIC: no Math.random; forkFrac is the caller's sweep knob,
|
||||
* every value octilinear, so the caller's min-crossing winner is still octilinear.
|
||||
*
|
||||
* @param {object} o
|
||||
* @param {{x:number,y:number}} o.lane the row's lane port on the margin.
|
||||
* @param {{id?:any,x:number,y:number}} o.a first pin (true de-overlapped coords, in its state).
|
||||
* @param {{id?:any,x:number,y:number}} o.b second pin.
|
||||
* @param {number} o.dirOut +1 or -1: horizontal sense from the lane toward the map.
|
||||
* @param {'row'|'trunk'} [o.mode='row']
|
||||
* @param {number} [o.forkFrac=0.5] 'trunk' only: fraction of the port→midpoint
|
||||
* horizontal budget the stem spends before forking (0..1).
|
||||
* @param {Array<{id?:any,x:number,y:number}>} [o.obstacles=[]] other pins to test legs against.
|
||||
* @param {number} [o.r=0] a leg "passes through" an obstacle if within r of it.
|
||||
* @returns {{
|
||||
* stem: Array<[number,number]>, legA: Array<[number,number]>, legB: Array<[number,number]>,
|
||||
* polys: Array<Array<[number,number]>>, // [stem+legA, stem+legB] — for DRAWING
|
||||
* segs: Array<[[number,number],[number,number]]>, // the fork's UNIQUE segments
|
||||
* (stem once + each leg), deduped. Count crossings BETWEEN forks by giving
|
||||
* every fork ONE polyline id — do NOT flatten polys into two ids, or a fork's
|
||||
* own two arms sharing the stem score as self-crossings. Feed one fork's segs
|
||||
* under one `li` to countPolyCrossings (as a single many-point run is not
|
||||
* possible for a branch, pass segs with a shared index).
|
||||
* octilinear: boolean,
|
||||
* throughPins: Array<any>, // obstacle ids (or indices) a leg passes through
|
||||
* d: string, // convenience SVG path (derived from polys)
|
||||
* }}
|
||||
*/
|
||||
export function forkRoute({ lane, a, b, dirOut, mode = 'row', forkFrac = 0.5, obstacles = [], r = 0 }) {
|
||||
if (!lane || !a || !b) throw new TypeError('lane, a and b are required');
|
||||
if (dirOut !== 1 && dirOut !== -1) throw new TypeError('dirOut must be +1 or -1');
|
||||
if (mode !== 'row' && mode !== 'trunk') throw new TypeError("mode must be 'row' or 'trunk'");
|
||||
|
||||
const STUB = 7;
|
||||
const ff = clamp01(forkFrac);
|
||||
|
||||
// The fork point. 'row' forks essentially at the port (a tiny stub so the two
|
||||
// legs don't start exactly on the margin). 'trunk' walks dirOut toward the
|
||||
// pair's midpoint x by forkFrac of the available horizontal budget.
|
||||
const midX = (a.x + b.x) / 2;
|
||||
const budget = Math.max(0, (midX - lane.x) * dirOut); // horizontal room port→midpoint in dirOut sense (≥0)
|
||||
const stemLen = mode === 'trunk' ? Math.max(STUB, ff * budget) : STUB;
|
||||
const forkX = lane.x + dirOut * stemLen;
|
||||
const forkPt = [forkX, lane.y];
|
||||
|
||||
const stem = [[lane.x, lane.y], forkPt];
|
||||
|
||||
const legA = octiLeg(forkPt, a, dirOut);
|
||||
const legB = octiLeg(forkPt, b, dirOut);
|
||||
|
||||
const polyA = [...stem, ...legA.slice(1)]; // legs start at forkPt; avoid duplicating it
|
||||
const polyB = [...stem, ...legB.slice(1)];
|
||||
const polys = [polyA, polyB];
|
||||
|
||||
// UNIQUE segments for crossing counts: the stem counted ONCE, then each leg.
|
||||
// (polys share the stem; counting them as two polylines would self-cross.)
|
||||
const segs = dedupeSegs([...toSegs(stem), ...toSegs(legA), ...toSegs(legB)]);
|
||||
|
||||
const octilinear = isOctilinear(polyA) && isOctilinear(polyB);
|
||||
|
||||
// Through-pin report: an obstacle the leg body passes within r of (excluding
|
||||
// the leg's own endpoint pin). Checked on legA/legB segments only — the stem
|
||||
// is shared and tested per-poly by the caller's crossing counter.
|
||||
const throughPins = [];
|
||||
if (obstacles.length && r > 0) {
|
||||
const own = new Set([idOf(a), idOf(b)]);
|
||||
for (const ob of obstacles) {
|
||||
const oid = idOf(ob);
|
||||
if (own.has(oid)) continue;
|
||||
if (segsPassThrough(polyA, ob, r) || segsPassThrough(polyB, ob, r)) throughPins.push(oid);
|
||||
}
|
||||
}
|
||||
|
||||
return { stem, legA, legB, polys, segs, octilinear, throughPins, d: toPath(polyA) + ' ' + toPath(polyB) };
|
||||
}
|
||||
|
||||
function toSegs(pts) {
|
||||
const out = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
|
||||
return out;
|
||||
}
|
||||
function dedupeSegs(segs) {
|
||||
const seen = new Set(), out = [];
|
||||
for (const s of segs) {
|
||||
const k = `${s[0][0]},${s[0][1]}|${s[1][0]},${s[1][1]}`;
|
||||
const k2 = `${s[1][0]},${s[1][1]}|${s[0][0]},${s[0][1]}`;
|
||||
if (seen.has(k) || seen.has(k2)) continue;
|
||||
seen.add(k); out.push(s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// One octilinear leg from the fork point to a pin: a 45° chamfer that closes the
|
||||
// shorter of |dx|,|dy|, then a straight H or V run for the remainder. This keeps
|
||||
// every segment H, V or exact-45° (mirrors octiRoute's feeler+diag, two-pin).
|
||||
function octiLeg(from, pin, dirOut) {
|
||||
const [fx, fy] = from;
|
||||
const dx = pin.x - fx;
|
||||
const dy = pin.y - fy;
|
||||
const adx = Math.abs(dx), ady = Math.abs(dy);
|
||||
// Diagonal leg closes min(adx,ady) on both axes at 45°; the straight remainder
|
||||
// finishes on whichever axis still has distance left.
|
||||
const diag = Math.min(adx, ady);
|
||||
const sx = Math.sign(dx) || dirOut; // if the pin is dead in line, lean outward
|
||||
const sy = Math.sign(dy) || 0;
|
||||
const corner = [fx + sx * diag, fy + sy * diag];
|
||||
// corner → pin is now purely horizontal (adx>ady) or vertical (ady>adx) or zero.
|
||||
const pts = [[fx, fy], corner];
|
||||
if (!almost(corner[0], pin.x) || !almost(corner[1], pin.y)) pts.push([pin.x, pin.y]);
|
||||
return pts;
|
||||
}
|
||||
|
||||
// ── octilinear invariant (same test + eps as poster-render.js) ────────────────
|
||||
function isOctilinear(pts, eps = 0.6) {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const dx = Math.abs(pts[i + 1][0] - pts[i][0]);
|
||||
const dy = Math.abs(pts[i + 1][1] - pts[i][1]);
|
||||
if (dx < eps || dy < eps) continue; // horizontal or vertical
|
||||
if (Math.abs(dx - dy) < eps) continue; // 45°
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does any segment of the polyline pass within r of point ob (not at an endpoint)?
|
||||
function segsPassThrough(pts, ob, r) {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const d = distPointToSeg(ob.x, ob.y, pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
|
||||
if (d <= r) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function distPointToSeg(px, py, ax, ay, bx, by) {
|
||||
const abx = bx - ax, aby = by - ay;
|
||||
const len2 = abx * abx + aby * aby;
|
||||
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
const x = ax + t * abx, y = ay + t * aby;
|
||||
return Math.hypot(px - x, py - y);
|
||||
}
|
||||
|
||||
function toPath(pts) {
|
||||
return pts.map((p, i) => `${i ? 'L' : 'M'}${round(p[0])},${round(p[1])}`).join(' ');
|
||||
}
|
||||
|
||||
const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
|
||||
const almost = (u, v) => Math.abs(u - v) < 1e-6;
|
||||
const round = (n) => Math.round(n * 100) / 100;
|
||||
const idOf = (p) => (p && p.id !== undefined ? p.id : p);
|
||||
|
||||
export default forkRoute;
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* pins-in-state.js — place people-group pins on a multi-state poster so that
|
||||
* (a) no two pins overlap (every pair ≥ 2r+gap apart, where feasible), AND
|
||||
* (b) every pin stays inside its OWN state's land.
|
||||
*
|
||||
* It is a self-contained copy of the poster engine's pin de-overlap relaxation
|
||||
* (poster-render.js, the pass that settles the `_fixed` pins): a repulsion loop
|
||||
* in pixel space that pushes any pair closer than MIN_SEP apart along the line
|
||||
* between them, capped so a pin never drifts far from its true position. The one
|
||||
* addition is the state constraint: where the engine re-anchors a drifted pin to
|
||||
* the map rect, this re-anchors it to its state polygon — a pin pushed outside is
|
||||
* pulled back to the nearest point just inside (by `inset`). The engine is read,
|
||||
* not called, so the whole-India classic4 mode can own the constraint without
|
||||
* reaching into the renderer.
|
||||
*
|
||||
* PX ONLY. Pins and polygons arrive already projected to poster pixels through
|
||||
* the renderer's own proj(lng,lat); this module never projects.
|
||||
*
|
||||
* DETERMINISTIC. Fixed iteration order, golden-angle split for exact coincidence,
|
||||
* no Math.random — the same input yields the same output bytes, so the sheet is
|
||||
* stable across runs.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {Array} opts.pins [{ id, x, y, stateId }] in poster px. Not mutated.
|
||||
* @param {object} opts.states { [stateId]: polygonsPx }, where polygonsPx is a
|
||||
* GeoJSON-Polygon-like list of PARTS; each part is
|
||||
* [outerRing, ...holeRings]; each ring is [[x,y],…]
|
||||
* in px. "Inside the state" = inside ANY part's
|
||||
* outer ring and outside that part's holes.
|
||||
* @param {number} opts.r pin radius in px.
|
||||
* @param {number} [opts.gap=0] extra clearance between pin edges; MIN_SEP = 2r+gap.
|
||||
* @param {number} [opts.iters=260] relaxation passes (matches the engine).
|
||||
* @param {number} [opts.maxDrift=58] hard cap on how far a pin may move from truth.
|
||||
* @param {number} [opts.inset=0.5] how far INSIDE the boundary a pulled-back pin lands.
|
||||
* @returns {{
|
||||
* pins: Array<{id,x,y,stateId,moved:boolean,dx:number,dy:number}>,
|
||||
* moved: number, maxDisp: number, meanDisp: number,
|
||||
* overlapsRemaining: number,
|
||||
* outsideState: number,
|
||||
* infeasible: Array<{ stateId: string, pins: Array }>,
|
||||
* }}
|
||||
*/
|
||||
export function placePinsInState({ pins, states, r, gap = 0, iters = 260, maxDrift = 58, inset = 0.5 }) {
|
||||
if (!Array.isArray(pins)) throw new TypeError('pins must be an array');
|
||||
if (!states || typeof states !== 'object') throw new TypeError('states must be an object');
|
||||
if (!(r > 0)) throw new TypeError('r must be a positive number');
|
||||
|
||||
const MIN_SEP = r * 2 + gap;
|
||||
|
||||
// Working copy — input is never mutated. _ox/_oy hold the true (projected) origin.
|
||||
const work = pins.map((p) => ({
|
||||
id: p.id, stateId: p.stateId,
|
||||
x: p.x, y: p.y, _ox: p.x, _oy: p.y,
|
||||
}));
|
||||
|
||||
// Resolve each pin's state parts once. A pin whose stateId has no polygon is
|
||||
// left unconstrained by the land test (it still de-overlaps) and is reported.
|
||||
const partsOf = (stateId) => {
|
||||
const poly = states[stateId];
|
||||
return Array.isArray(poly) ? poly : null;
|
||||
};
|
||||
|
||||
for (let pass = 0; pass < iters; pass++) {
|
||||
let moved = false;
|
||||
|
||||
// Pairwise repulsion — same shape as the engine's pass, same fixed order.
|
||||
for (let i = 0; i < work.length; i++) {
|
||||
for (let j = i + 1; j < work.length; j++) {
|
||||
const a = work[i], b = work[j];
|
||||
let dx = b.x - a.x, dy = b.y - a.y;
|
||||
let dist = Math.hypot(dx, dy);
|
||||
if (dist >= MIN_SEP) continue;
|
||||
if (dist < 0.0001) { // exactly coincident — split on a
|
||||
const ang = i * 2.399963; // golden angle, deterministic
|
||||
dx = Math.cos(ang); dy = Math.sin(ang); dist = 1;
|
||||
}
|
||||
const push = (MIN_SEP - dist) / 2 + 0.15;
|
||||
const ux = dx / dist, uy = dy / dist;
|
||||
a.x -= ux * push; a.y -= uy * push;
|
||||
b.x += ux * push; b.y += uy * push;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-anchor: cap drift from truth, then keep each pin inside its own state.
|
||||
for (const it of work) {
|
||||
const ddx = it.x - it._ox, ddy = it.y - it._oy;
|
||||
const d = Math.hypot(ddx, ddy);
|
||||
if (d > maxDrift) { it.x = it._ox + (ddx / d) * maxDrift; it.y = it._oy + (ddy / d) * maxDrift; }
|
||||
const parts = partsOf(it.stateId);
|
||||
if (parts && !pointInState(it.x, it.y, parts)) {
|
||||
const near = nearestInside(it.x, it.y, parts, inset);
|
||||
it.x = near.x; it.y = near.y;
|
||||
}
|
||||
}
|
||||
|
||||
if (!moved) break;
|
||||
}
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────────
|
||||
let maxDisp = 0, sumDisp = 0, movedCount = 0, outsideState = 0;
|
||||
const outPins = work.map((it) => {
|
||||
const dx = it.x - it._ox, dy = it.y - it._oy;
|
||||
const disp = Math.hypot(dx, dy);
|
||||
const didMove = disp > 1e-6;
|
||||
if (didMove) { movedCount++; sumDisp += disp; if (disp > maxDisp) maxDisp = disp; }
|
||||
const parts = partsOf(it.stateId);
|
||||
if (parts && !pointInState(it.x, it.y, parts)) outsideState++;
|
||||
return { id: it.id, x: it.x, y: it.y, stateId: it.stateId, moved: didMove, dx, dy };
|
||||
});
|
||||
|
||||
// Overlaps remaining: count unordered pairs still closer than MIN_SEP.
|
||||
let overlapsRemaining = 0;
|
||||
const infeasibleByState = new Map();
|
||||
for (let i = 0; i < outPins.length; i++) {
|
||||
for (let j = i + 1; j < outPins.length; j++) {
|
||||
const a = outPins[i], b = outPins[j];
|
||||
if (Math.hypot(b.x - a.x, b.y - a.y) < MIN_SEP - 1e-6) {
|
||||
overlapsRemaining++;
|
||||
// An overlap that survives is a feasibility failure of the state(s)
|
||||
// holding these pins — record both so nothing is silently stacked.
|
||||
for (const p of [a, b]) {
|
||||
if (!infeasibleByState.has(p.stateId)) infeasibleByState.set(p.stateId, new Set());
|
||||
infeasibleByState.get(p.stateId).add(p.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const infeasible = [...infeasibleByState.entries()]
|
||||
.map(([stateId, ids]) => ({ stateId, pins: [...ids] }))
|
||||
.sort((p, q) => String(p.stateId).localeCompare(String(q.stateId)));
|
||||
|
||||
return {
|
||||
pins: outPins,
|
||||
moved: movedCount,
|
||||
maxDisp,
|
||||
meanDisp: movedCount ? sumDisp / movedCount : 0,
|
||||
overlapsRemaining,
|
||||
outsideState,
|
||||
infeasible,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Geometry (self-contained; px space) ───────────────────────────────────────
|
||||
|
||||
// Ray-casting point-in-ring. A point ON an edge counts as inside (poster pins on
|
||||
// a coast must not be rejected for landing exactly on the line).
|
||||
function pointInRing(x, y, ring) {
|
||||
let inside = false;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const xi = ring[i][0], yi = ring[i][1];
|
||||
const xk = ring[k][0], yk = ring[k][1];
|
||||
const intersect = ((yi > y) !== (yk > y)) &&
|
||||
(x < ((xk - xi) * (y - yi)) / ((yk - yi) || 1e-12) + xi);
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// Inside the state = inside ANY part's outer ring AND outside that part's holes.
|
||||
function pointInState(x, y, parts) {
|
||||
for (const part of parts) {
|
||||
if (!part || !part.length) continue;
|
||||
const [outer, ...holes] = part;
|
||||
if (!outer || outer.length < 3) continue;
|
||||
if (!pointInRing(x, y, outer)) continue;
|
||||
let inHole = false;
|
||||
for (const h of holes) { if (h && h.length >= 3 && pointInRing(x, y, h)) { inHole = true; break; } }
|
||||
if (!inHole) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nearest point ON a ring's edges to (x,y), as {x,y,d2}.
|
||||
function nearestOnRing(x, y, ring) {
|
||||
let best = null;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const p = nearestOnSegment(x, y, ring[k][0], ring[k][1], ring[i][0], ring[i][1]);
|
||||
if (!best || p.d2 < best.d2) best = p;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function nearestOnSegment(px, py, ax, ay, bx, by) {
|
||||
const abx = bx - ax, aby = by - ay;
|
||||
const len2 = abx * abx + aby * aby;
|
||||
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
const x = ax + t * abx, y = ay + t * aby;
|
||||
const dx = px - x, dy = py - y;
|
||||
return { x, y, d2: dx * dx + dy * dy };
|
||||
}
|
||||
|
||||
// Pull an outside point to the nearest point just INSIDE the state, by `inset`.
|
||||
// Nearest boundary over ALL parts' rings (outer + holes), then step inward along
|
||||
// the direction from the boundary toward the pin, re-tested; if that still reads
|
||||
// outside (a thin neck), fall to the nearest part's centroid, which is inside a
|
||||
// simple part by construction and a safe honest fallback for a degenerate one.
|
||||
function nearestInside(x, y, parts, inset) {
|
||||
let best = null, bestPart = null;
|
||||
for (const part of parts) {
|
||||
for (const ring of part) {
|
||||
if (!ring || ring.length < 2) continue;
|
||||
const cand = nearestOnRing(x, y, ring);
|
||||
if (cand && (!best || cand.d2 < best.d2)) { best = cand; bestPart = part; }
|
||||
}
|
||||
}
|
||||
if (!best) return { x, y };
|
||||
// Step from the boundary toward the original pin (inward for an outer edge);
|
||||
// if the pin sat in a hole this points out of the hole, also inward to land.
|
||||
const vx = x - best.x, vy = y - best.y;
|
||||
const vlen = Math.hypot(vx, vy) || 1;
|
||||
let nx = best.x + (vx / vlen) * inset;
|
||||
let ny = best.y + (vy / vlen) * inset;
|
||||
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||
// Thin-neck fallback: step the other way, then the part centroid.
|
||||
nx = best.x - (vx / vlen) * inset; ny = best.y - (vy / vlen) * inset;
|
||||
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||
const c = ringCentroid(bestPart[0]);
|
||||
return pointInState(c.x, c.y, parts) ? c : { x: best.x, y: best.y };
|
||||
}
|
||||
|
||||
function ringCentroid(ring) {
|
||||
let a = 0, cx = 0, cy = 0;
|
||||
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||
const cross = ring[k][0] * ring[i][1] - ring[i][0] * ring[k][1];
|
||||
a += cross; cx += (ring[k][0] + ring[i][0]) * cross; cy += (ring[k][1] + ring[i][1]) * cross;
|
||||
}
|
||||
if (Math.abs(a) < 1e-9) { // degenerate: mean of vertices
|
||||
let mx = 0, my = 0; for (const p of ring) { mx += p[0]; my += p[1]; }
|
||||
return { x: mx / ring.length, y: my / ring.length };
|
||||
}
|
||||
return { x: cx / (3 * a), y: cy / (3 * a) };
|
||||
}
|
||||
|
||||
export default placePinsInState;
|
||||
2982
plugin-designs/bless-india-posters/src/generators/poster-render.js
Normal file
2982
plugin-designs/bless-india-posters/src/generators/poster-render.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Key Cities / Location Clusters",
|
||||
"grain": "one row per ~25km location cluster",
|
||||
"caveat": "A cluster is people-group CENTROIDS, not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"caveat": "A cluster is people-group PINS (each marks a people group, not a town), not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"internet_caveat": "Internet figures are NATIONAL. Per-city connectivity is not publicly available at this scale."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
|
|
@ -1446,8 +1446,8 @@ function switchTab(tabId) {
|
|||
// search query doesn't leak across tabs (qa-buildinng-round-1 Bug 11).
|
||||
// clearGeocoderProgrammatic uses its internal _geoBeingCleared to skip the legend-
|
||||
// deselect side effect — pure text reset. Flip the FEATURES flag to keep
|
||||
// search queries across tabs (the user said "could go either way" — sticking
|
||||
// with clear-on-switch as the default per A4).
|
||||
// search queries across tabs (either behaviour is acceptable; clear-on-switch
|
||||
// stays the default per A4).
|
||||
if (!FEATURES.geocoderPersistAcrossTabs) clearGeocoderProgrammatic()
|
||||
const tab = tabs.value.find(t => t.id === tabId)
|
||||
if (!tab || !map.value) return
|
||||
|
|
|
|||
|
|
@ -570,7 +570,9 @@ const HEAVY_ELEMENTS = new Set([
|
|||
// The two composites here are exactly the two bundle ids that use StackLayout as their OWN root —
|
||||
// same identification method as HEAVY_ELEMENTS (measured against the tree, not guessed), and it is
|
||||
// not a coincidence that both entries already appear in that set too (they also nest a map).
|
||||
const SCROLL_ELEMENTS = new Set(['dashboard-plus-grid', 'research-map-plus-grid'])
|
||||
// bless-india-posters is a natural-height page of poster cards (its owner's mount note): pinned, the grid
|
||||
// would be trapped in one viewport, so it scrolls in the pane like the two composites.
|
||||
const SCROLL_ELEMENTS = new Set(['dashboard-plus-grid', 'research-map-plus-grid', 'bless-india-posters'])
|
||||
|
||||
// The set of bundle ids the CURATED sections above already place, derived from NAV_MODEL + TAB_DEFS
|
||||
// rather than hand-written. Deriving it means the "is this already placed?" answer can never drift
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Toggle list", openViews: "Open the list of views", expandList: "Expand list", collapseList: "Collapse list", collapse: "Collapse", home: "Back to homepage",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Prayer Walk Planner", 'tab.upg-neighbor': "UPG Neighbor", 'tab.key-cities': "Key Cities", 'tab.all-people-groups': "All People Groups", 'tab.pplr-data-maps': "PPLR Data Maps", 'tab.wall-of-people-groups': "Wall of People Groups", 'tab.country-posters': "Country Posters", 'sub.country-posters': "a poster for every country", 'tab.poster-design': "Poster Design", 'sub.poster-design': "every poster style and size", 'tab.doxa-vision-card': "Doxa Vision Card",
|
||||
'tab.prayer-walk-planner': "Prayer Walk Planner", 'tab.upg-neighbor': "UPG Neighbor", 'tab.key-cities': "Key Cities", 'tab.all-people-groups': "All People Groups", 'tab.pplr-data-maps': "PPLR Data Maps", 'tab.wall-of-people-groups': "Wall of People Groups", 'tab.bless-india-posters': "Bless India Posters", 'tab.country-posters': "Country Posters", 'sub.country-posters': "a poster for every country", 'tab.poster-design': "Poster Design", 'sub.poster-design': "every poster style and size", 'tab.doxa-vision-card': "Doxa Vision Card",
|
||||
},
|
||||
es: {
|
||||
backToTop: 'Volver arriba',
|
||||
|
|
@ -55,7 +55,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Mostrar u ocultar la lista", openViews: "Abrir la lista de vistas", expandList: "Expandir la lista", collapseList: "Contraer la lista", collapse: "Contraer", home: "Volver a la página de inicio",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Caminatas de oración", 'tab.upg-neighbor': "Vecino UPG", 'tab.key-cities': "Ciudades clave", 'tab.all-people-groups': "Todos los pueblos", 'tab.pplr-data-maps': "Mapas de datos PPLR", 'tab.wall-of-people-groups': "Muro de los pueblos", 'tab.country-posters': "Carteles por país", 'sub.country-posters': "un cartel para cada país", 'tab.poster-design': "Diseño de carteles", 'sub.poster-design': "todos los estilos y tamaños de cartel", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
|
||||
'tab.prayer-walk-planner': "Caminatas de oración", 'tab.upg-neighbor': "Vecino UPG", 'tab.key-cities': "Ciudades clave", 'tab.all-people-groups': "Todos los pueblos", 'tab.pplr-data-maps': "Mapas de datos PPLR", 'tab.wall-of-people-groups': "Muro de los pueblos", 'tab.bless-india-posters': "Pósteres «Bendice a la India»", 'tab.country-posters': "Carteles por país", 'sub.country-posters': "un cartel para cada país", 'tab.poster-design': "Diseño de carteles", 'sub.poster-design': "todos los estilos y tamaños de cartel", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
|
||||
},
|
||||
fr: {
|
||||
backToTop: 'Retour en haut',
|
||||
|
|
@ -76,7 +76,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Afficher ou masquer la liste", openViews: "Ouvrir la liste des vues", expandList: "Déplier la liste", collapseList: "Replier la liste", collapse: "Replier", home: "Retour à l’accueil",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Marches de prière", 'tab.upg-neighbor': "Voisin UPG", 'tab.key-cities': "Villes clés", 'tab.all-people-groups': "Tous les peuples", 'tab.pplr-data-maps': "Cartes de données PPLR", 'tab.wall-of-people-groups': "Mur des peuples", 'tab.country-posters': "Affiches par pays", 'sub.country-posters': "une affiche pour chaque pays", 'tab.poster-design': "Conception d’affiches", 'sub.poster-design': "tous les styles et formats d’affiche", 'tab.doxa-vision-card': "Carte de vision Doxa",
|
||||
'tab.prayer-walk-planner': "Marches de prière", 'tab.upg-neighbor': "Voisin UPG", 'tab.key-cities': "Villes clés", 'tab.all-people-groups': "Tous les peuples", 'tab.pplr-data-maps': "Cartes de données PPLR", 'tab.wall-of-people-groups': "Mur des peuples", 'tab.bless-india-posters': "Affiches « Bénis l’Inde »", 'tab.country-posters': "Affiches par pays", 'sub.country-posters': "une affiche pour chaque pays", 'tab.poster-design': "Conception d’affiches", 'sub.poster-design': "tous les styles et formats d’affiche", 'tab.doxa-vision-card': "Carte de vision Doxa",
|
||||
},
|
||||
de: {
|
||||
backToTop: 'Nach oben',
|
||||
|
|
@ -97,7 +97,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Liste ein-/ausblenden", openViews: "Ansichtsliste öffnen", expandList: "Liste ausklappen", collapseList: "Liste einklappen", collapse: "Einklappen", home: "Zurück zur Startseite",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Gebetsspaziergänge", 'tab.upg-neighbor': "UPG-Nachbar", 'tab.key-cities': "Schlüsselstädte", 'tab.all-people-groups': "Alle Volksgruppen", 'tab.pplr-data-maps': "PPLR-Datenkarten", 'tab.wall-of-people-groups': "Wand der Volksgruppen", 'tab.country-posters': "Länderposter", 'sub.country-posters': "ein Poster für jedes Land", 'tab.poster-design': "Postergestaltung", 'sub.poster-design': "alle Posterstile und -größen", 'tab.doxa-vision-card': "Doxa-Visionskarte",
|
||||
'tab.prayer-walk-planner': "Gebetsspaziergänge", 'tab.upg-neighbor': "UPG-Nachbar", 'tab.key-cities': "Schlüsselstädte", 'tab.all-people-groups': "Alle Volksgruppen", 'tab.pplr-data-maps': "PPLR-Datenkarten", 'tab.wall-of-people-groups': "Wand der Volksgruppen", 'tab.bless-india-posters': "„Segne Indien“-Poster", 'tab.country-posters': "Länderposter", 'sub.country-posters': "ein Poster für jedes Land", 'tab.poster-design': "Postergestaltung", 'sub.poster-design': "alle Posterstile und -größen", 'tab.doxa-vision-card': "Doxa-Visionskarte",
|
||||
},
|
||||
pt: {
|
||||
backToTop: 'Voltar ao topo',
|
||||
|
|
@ -118,7 +118,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Mostrar ou ocultar a lista", openViews: "Abrir a lista de visualizações", expandList: "Expandir a lista", collapseList: "Recolher a lista", collapse: "Recolher", home: "Voltar à página inicial",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Caminhadas de oração", 'tab.upg-neighbor': "Vizinho UPG", 'tab.key-cities': "Cidades-chave", 'tab.all-people-groups': "Todos os povos", 'tab.pplr-data-maps': "Mapas de dados PPLR", 'tab.wall-of-people-groups': "Mural dos povos", 'tab.country-posters': "Cartazes por país", 'sub.country-posters': "um cartaz para cada país", 'tab.poster-design': "Design de cartazes", 'sub.poster-design': "todos os estilos e tamanhos de cartaz", 'tab.doxa-vision-card': "Cartão de visão Doxa",
|
||||
'tab.prayer-walk-planner': "Caminhadas de oração", 'tab.upg-neighbor': "Vizinho UPG", 'tab.key-cities': "Cidades-chave", 'tab.all-people-groups': "Todos os povos", 'tab.pplr-data-maps': "Mapas de dados PPLR", 'tab.wall-of-people-groups': "Mural dos povos", 'tab.bless-india-posters': "Pôsteres «Abençoe a Índia»", 'tab.country-posters': "Cartazes por país", 'sub.country-posters': "um cartaz para cada país", 'tab.poster-design': "Design de cartazes", 'sub.poster-design': "todos os estilos e tamanhos de cartaz", 'tab.doxa-vision-card': "Cartão de visão Doxa",
|
||||
},
|
||||
it: {
|
||||
backToTop: 'Torna su',
|
||||
|
|
@ -141,7 +141,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Mostra o nascondi l’elenco", openViews: "Apri l’elenco delle viste", expandList: "Espandi l’elenco", collapseList: "Comprimi l’elenco", collapse: "Comprimi", home: "Torna alla home page",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Passeggiate di preghiera", 'tab.upg-neighbor': "Vicino UPG", 'tab.key-cities': "Città chiave", 'tab.all-people-groups': "Tutti i popoli", 'tab.pplr-data-maps': "Mappe dati PPLR", 'tab.wall-of-people-groups': "Muro dei popoli", 'tab.country-posters': "Poster per paese", 'sub.country-posters': "un poster per ogni paese", 'tab.poster-design': "Design dei poster", 'sub.poster-design': "tutti gli stili e i formati dei poster", 'tab.doxa-vision-card': "Scheda visione Doxa",
|
||||
'tab.prayer-walk-planner': "Passeggiate di preghiera", 'tab.upg-neighbor': "Vicino UPG", 'tab.key-cities': "Città chiave", 'tab.all-people-groups': "Tutti i popoli", 'tab.pplr-data-maps': "Mappe dati PPLR", 'tab.wall-of-people-groups': "Muro dei popoli", 'tab.bless-india-posters': "Poster «Benedici l’India»", 'tab.country-posters': "Poster per paese", 'sub.country-posters': "un poster per ogni paese", 'tab.poster-design': "Design dei poster", 'sub.poster-design': "tutti gli stili e i formati dei poster", 'tab.doxa-vision-card': "Scheda visione Doxa",
|
||||
},
|
||||
ro: {
|
||||
backToTop: 'Sus',
|
||||
|
|
@ -162,7 +162,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Afișează sau ascunde lista", openViews: "Deschide lista de vizualizări", expandList: "Extinde lista", collapseList: "Restrânge lista", collapse: "Restrânge", home: "Înapoi la pagina principală",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Plimbări de rugăciune", 'tab.upg-neighbor': "Vecin UPG", 'tab.key-cities': "Orașe-cheie", 'tab.all-people-groups': "Toate popoarele", 'tab.pplr-data-maps': "Hărți de date PPLR", 'tab.wall-of-people-groups': "Zidul popoarelor", 'tab.country-posters': "Afișe pe țări", 'sub.country-posters': "un afiș pentru fiecare țară", 'tab.poster-design': "Design de afișe", 'sub.poster-design': "toate stilurile și dimensiunile de afiș", 'tab.doxa-vision-card': "Card de viziune Doxa",
|
||||
'tab.prayer-walk-planner': "Plimbări de rugăciune", 'tab.upg-neighbor': "Vecin UPG", 'tab.key-cities': "Orașe-cheie", 'tab.all-people-groups': "Toate popoarele", 'tab.pplr-data-maps': "Hărți de date PPLR", 'tab.wall-of-people-groups': "Zidul popoarelor", 'tab.bless-india-posters': "Postere „Binecuvântează India”", 'tab.country-posters': "Afișe pe țări", 'sub.country-posters': "un afiș pentru fiecare țară", 'tab.poster-design': "Design de afișe", 'sub.poster-design': "toate stilurile și dimensiunile de afiș", 'tab.doxa-vision-card': "Card de viziune Doxa",
|
||||
},
|
||||
ru: {
|
||||
backToTop: 'Наверх',
|
||||
|
|
@ -183,7 +183,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "Показать или скрыть список", openViews: "Открыть список представлений", expandList: "Развернуть список", collapseList: "Свернуть список", collapse: "Свернуть", home: "На главную страницу",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Молитвенные прогулки", 'tab.upg-neighbor': "Сосед UPG", 'tab.key-cities': "Ключевые города", 'tab.all-people-groups': "Все народы", 'tab.pplr-data-maps': "Карты данных PPLR", 'tab.wall-of-people-groups': "Стена народов", 'tab.country-posters': "Плакаты по странам", 'sub.country-posters': "плакат для каждой страны", 'tab.poster-design': "Дизайн плакатов", 'sub.poster-design': "все стили и размеры плакатов", 'tab.doxa-vision-card': "Карточка видения Doxa",
|
||||
'tab.prayer-walk-planner': "Молитвенные прогулки", 'tab.upg-neighbor': "Сосед UPG", 'tab.key-cities': "Ключевые города", 'tab.all-people-groups': "Все народы", 'tab.pplr-data-maps': "Карты данных PPLR", 'tab.wall-of-people-groups': "Стена народов", 'tab.bless-india-posters': "Плакаты «Благослови Индию»", 'tab.country-posters': "Плакаты по странам", 'sub.country-posters': "плакат для каждой страны", 'tab.poster-design': "Дизайн плакатов", 'sub.poster-design': "все стили и размеры плакатов", 'tab.doxa-vision-card': "Карточка видения Doxa",
|
||||
},
|
||||
ar: {
|
||||
backToTop: 'إلى الأعلى',
|
||||
|
|
@ -204,7 +204,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "إظهار القائمة أو إخفاؤها", openViews: "فتح قائمة العروض", expandList: "توسيع القائمة", collapseList: "طي القائمة", collapse: "طي", home: "العودة إلى الصفحة الرئيسية",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "جولات الصلاة", 'tab.upg-neighbor': "جار UPG", 'tab.key-cities': "المدن الرئيسية", 'tab.all-people-groups': "جميع الشعوب", 'tab.pplr-data-maps': "خرائط بيانات PPLR", 'tab.wall-of-people-groups': "جدار الشعوب", 'tab.country-posters': "ملصقات الدول", 'sub.country-posters': "ملصق لكل دولة", 'tab.poster-design': "تصميم الملصقات", 'sub.poster-design': "كل أنماط الملصقات وأحجامها", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
|
||||
'tab.prayer-walk-planner': "جولات الصلاة", 'tab.upg-neighbor': "جار UPG", 'tab.key-cities': "المدن الرئيسية", 'tab.all-people-groups': "جميع الشعوب", 'tab.pplr-data-maps': "خرائط بيانات PPLR", 'tab.wall-of-people-groups': "جدار الشعوب", 'tab.bless-india-posters': "ملصقات باركوا الهند", 'tab.country-posters': "ملصقات الدول", 'sub.country-posters': "ملصق لكل دولة", 'tab.poster-design': "تصميم الملصقات", 'sub.poster-design': "كل أنماط الملصقات وأحجامها", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
|
||||
},
|
||||
hi: {
|
||||
backToTop: 'ऊपर जाएँ',
|
||||
|
|
@ -225,7 +225,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "सूची दिखाएँ या छिपाएँ", openViews: "दृश्यों की सूची खोलें", expandList: "सूची फैलाएँ", collapseList: "सूची संक्षिप्त करें", collapse: "संक्षिप्त करें", home: "होमपेज पर वापस जाएँ",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "प्रार्थना पदयात्रा", 'tab.upg-neighbor': "UPG पड़ोसी", 'tab.key-cities': "प्रमुख शहर", 'tab.all-people-groups': "सभी जनसमूह", 'tab.pplr-data-maps': "PPLR डेटा मानचित्र", 'tab.wall-of-people-groups': "जनसमूहों की दीवार", 'tab.country-posters': "देश पोस्टर", 'sub.country-posters': "हर देश के लिए एक पोस्टर", 'tab.poster-design': "पोस्टर डिज़ाइन", 'sub.poster-design': "हर पोस्टर शैली और आकार", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
|
||||
'tab.prayer-walk-planner': "प्रार्थना पदयात्रा", 'tab.upg-neighbor': "UPG पड़ोसी", 'tab.key-cities': "प्रमुख शहर", 'tab.all-people-groups': "सभी जनसमूह", 'tab.pplr-data-maps': "PPLR डेटा मानचित्र", 'tab.wall-of-people-groups': "जनसमूहों की दीवार", 'tab.bless-india-posters': "भारत को आशीष दें — पोस्टर", 'tab.country-posters': "देश पोस्टर", 'sub.country-posters': "हर देश के लिए एक पोस्टर", 'tab.poster-design': "पोस्टर डिज़ाइन", 'sub.poster-design': "हर पोस्टर शैली और आकार", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
|
||||
},
|
||||
zh: {
|
||||
backToTop: '回到顶部',
|
||||
|
|
@ -246,7 +246,7 @@ export const STRINGS = {
|
|||
// (plugin studio's catalog) so both shells name the same buttons the same way.
|
||||
toggleList: "显示或隐藏列表", openViews: "打开视图列表", expandList: "展开列表", collapseList: "折叠列表", collapse: "折叠", home: "返回首页",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "祷告行走", 'tab.upg-neighbor': "UPG 邻居", 'tab.key-cities': "重点城市", 'tab.all-people-groups': "所有民族", 'tab.pplr-data-maps': "PPLR 数据地图", 'tab.wall-of-people-groups': "民族之墙", 'tab.country-posters': "国家海报", 'sub.country-posters': "每个国家一张海报", 'tab.poster-design': "海报设计", 'sub.poster-design': "所有海报样式与尺寸", 'tab.doxa-vision-card': "Doxa 异象卡",
|
||||
'tab.prayer-walk-planner': "祷告行走", 'tab.upg-neighbor': "UPG 邻居", 'tab.key-cities': "重点城市", 'tab.all-people-groups': "所有民族", 'tab.pplr-data-maps': "PPLR 数据地图", 'tab.wall-of-people-groups': "民族之墙", 'tab.bless-india-posters': "祝福印度海报", 'tab.country-posters': "国家海报", 'sub.country-posters': "每个国家一张海报", 'tab.poster-design': "海报设计", 'sub.poster-design': "所有海报样式与尺寸", 'tab.doxa-vision-card': "Doxa 异象卡",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1802,8 +1802,8 @@ function switchTab(tabId) {
|
|||
// search query doesn't leak across tabs (qa-buildinng-round-1 Bug 11).
|
||||
// clearGeocoderProgrammatic uses its internal _geoBeingCleared to skip the legend-
|
||||
// deselect side effect — pure text reset. Flip the FEATURES flag to keep
|
||||
// search queries across tabs (the user said "could go either way" — sticking
|
||||
// with clear-on-switch as the default per A4).
|
||||
// search queries across tabs (either behaviour is acceptable; clear-on-switch
|
||||
// stays the default per A4).
|
||||
if (!FEATURES.geocoderPersistAcrossTabs) clearGeocoderProgrammatic()
|
||||
const tab = tabs.value.find(t => t.id === tabId)
|
||||
if (!tab || !map.value) return
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ variant, so "Jämtland" and "Jamtland" reach the same zone.
|
|||
|
||||
These are constraints on the product, not preferences:
|
||||
|
||||
- **Pins are approximate.** A dot marks where a people is found in a country, not a town. The notice is
|
||||
- **Pins are approximate.** A dot marks a people group, not a town. The notice is
|
||||
shown on the map and in every drawer, and provinces are marked approximate everywhere.
|
||||
- **Never "reached".** The words are "resource found in the sources checked" and "nothing found in own
|
||||
language". No game words — nothing is "claimed" or "done".
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ useShadowStyles(
|
|||
</header>
|
||||
<GiCombos />
|
||||
<p class="gb-note" data-notice="standing">
|
||||
{{ T('gospelInternet.notice.standing', 'Dots are approximate (where a people is found in its country, not a town). Internet figures are the country\'s, or the Indian state\'s, never the group\'s. "Found" means found in the sources checked.') }}
|
||||
{{ T('gospelInternet.notice.standing', 'Dots are approximate: a dot marks a people group, not a town. Internet figures are the country\'s, or the Indian state\'s, never the group\'s. "Found" means found in the sources checked.') }}
|
||||
<span v-if="sourcesLine"> {{ T('gospelInternet.notice.sources', 'Sources:') }} {{ sourcesLine }}.</span>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ if (failures.length) {
|
|||
coverage,
|
||||
band_rule: bandingText,
|
||||
internet_note: 'internet_pct is the COUNTRY\'s figure (World Bank), never the group\'s. state_internet_* is India\'s state figure from NFHS-5 (2019-21): the share of women and of men aged 15-49 who have EVER used the internet, kept apart, never averaged, never on a scale with subscriptions per 100.',
|
||||
province_note: 'province is read from gospel-by-province-groups.json: placed by point-in-polygon on the group\'s coordinate, which marks where a PEOPLE is found, not a town. It is approximate, and near a border it can fall on the wrong side; it is never a fact about connectivity in that state.',
|
||||
province_note: 'province is read from gospel-by-province-groups.json: placed by point-in-polygon on the group\'s coordinate, which marks a PEOPLE group, not a town. It is approximate, and near a border it can fall on the wrong side; it is never a fact about connectivity in that state.',
|
||||
access_note: 'access_detail says what exists in the group\'s OWN language in the sources checked; "none" means nothing was found there, never that nothing exists. access_level: own = something in their language; bridge = nothing in their language but a bridge, candidate or Deaf way in; zero = neither.',
|
||||
platforms: {
|
||||
messaging_source: msg.meta?.source?.name || '', messaging_accessed: msg.meta?.source?.accessed || '',
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ useShadowStyles(
|
|||
</div>
|
||||
<div class="gid-body">
|
||||
<p class="gid-notice" data-notice="approximate-pin">
|
||||
{{ T('gospelInternet.notice.pin', 'The dot marks where this people is found in {country}, not a town.', { country: r.country }) }}
|
||||
{{ T('gospelInternet.notice.pin', 'The dot marks this people group, not a town.') }}
|
||||
<template v-if="r.shares_point"> {{ T('gospelInternet.notice.shares', 'It shares its point with {n} other group(s).', { n: r.shares_point }) }}</template>
|
||||
</p>
|
||||
<section v-for="s in sections" :key="s.id" class="gid-sec" :data-section="s.id">
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const sections = computed(() => {
|
|||
const prov = counts('province')
|
||||
if (prov.length) {
|
||||
out.push({ id: 'province', title: T('gospelInternet.facet.province', 'India and China: state or province'),
|
||||
note: T('gospelInternet.facet.provinceNote', 'Approximate: placed from where a people is found, not a town.'), long: true, groups: [
|
||||
note: T('gospelInternet.facet.provinceNote', 'Approximate: taken from the group\'s dot on the map, not from a town.'), long: true, groups: [
|
||||
{ key: 'province', rows: prov, label: (v) => v },
|
||||
] })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function setGlobal(name, value) {
|
|||
* hoverKey POINTS — an outline on the hovered pin, from the grid or the map. It never flies,
|
||||
* never selects, never filters.
|
||||
*
|
||||
* HONESTY: a pin marks where a people is found within a country, not a town (the legend says so, and
|
||||
* HONESTY: a pin marks a people group, not a town, and its position is approximate (the legend says so, and
|
||||
* how many share a point); the band is the COUNTRY's figure; a state's NFHS-5 figure is the state's.
|
||||
*/
|
||||
import { ref, shallowRef, computed, watch, onMounted, onBeforeUnmount, markRaw, inject } from 'vue'
|
||||
|
|
@ -412,7 +412,7 @@ useShadowStyles(
|
|||
<button type="button" :aria-pressed="provinceMode === 'men'" @click="setProvinceMode('men')">{{ T('gospelInternet.legend.modeMen', 'Internet: men') }}</button>
|
||||
</div>
|
||||
<div v-if="provinceMode !== 'zero'" class="gim-note">{{ T('gospelInternet.legend.nfhs', 'India only: share of women or men aged 15-49 who have ever used the internet, by state. NFHS-5, 2019-21. China has no verified provincial source.') }} {{ nfhsAttribution }}</div>
|
||||
<div class="gim-note">{{ T('gospelInternet.notice.pins', 'A dot marks where a people is found in its country, not a town. {n} groups share a point with another group.', { n: approxCount.toLocaleString() }) }}</div>
|
||||
<div class="gim-note">{{ T('gospelInternet.notice.pins', 'A dot marks a people group, not a town. {n} groups share a point with another group.', { n: approxCount.toLocaleString() }) }}</div>
|
||||
</div>
|
||||
<button v-else type="button" class="gim-legend-min" @click="legendOpen = true">{{ T('gospelInternet.legend.show', 'Legend') }}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1168,7 +1168,7 @@ const withoutResources = computed(() => groupsForCountry.value.filter((r) => !ha
|
|||
* coastal Karnataka; four more share a second point a few kilometres away.
|
||||
*
|
||||
* The coordinates also carry FALSE PRECISION: 1,274 of them are given to six decimal places,
|
||||
* which is metre resolution on a figure that describes where a people is found. Rendering that
|
||||
* which is metre resolution on a point that is only approximate (locations are generalised). Rendering that
|
||||
* without comment invites a reader to treat a dot as an address.
|
||||
*
|
||||
* So the sheet says what the dot is, and where a point is shared it says how many share it.
|
||||
|
|
@ -2620,8 +2620,8 @@ useShadowStyles(
|
|||
<b>{{ fmt(selectedProvince.nobody_praying_groups) }} / {{ fmt(selectedProvince.prayer_pages) }}</b>
|
||||
</div>
|
||||
<p class="gm-note" dir="ltr" style="margin:6px 0 0">
|
||||
A group is placed in a province from its own coordinate, which marks where a people is
|
||||
found rather than a settlement. Near a border that point can fall on the wrong side.
|
||||
A group is placed in a province from its dot on the map, which marks a people group, not a
|
||||
settlement. The dot's position is approximate, so near a border it can fall on the wrong side.
|
||||
</p>
|
||||
<!-- The THIRD requirement at the province grain: clicking a place filters the
|
||||
table to it. Same split as the country and language links — a filter when the tables
|
||||
|
|
@ -2811,7 +2811,7 @@ useShadowStyles(
|
|||
<div class="gm-stat"><span>{{ t('gospel.stat.countries') }}</span><b>{{ selectedGroup.countries }}</b></div>
|
||||
|
||||
<!-- ═══ WHAT THE DOT MEANS, SAID ON THE SHEET THAT THE DOT OPENED ══════════════════
|
||||
A pin marks where a PEOPLE is found, not a settlement, and the data shows it: 108
|
||||
A pin marks a PEOPLE group, not a settlement, and its position is approximate: 108
|
||||
of the 2,107 placed groups sit on a coordinate they share with another group. Where
|
||||
this one does, it names them, because "approximate" on its own hides the part a
|
||||
reader would never otherwise discover — that some dots are literally the same dot.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
* file was cut to the provinces where the unreached peoples actually are.
|
||||
*
|
||||
* ⚠ AND THE ASSIGNMENT IS ONLY AS GOOD AS THE PIN. A group's province comes from a
|
||||
* point-in-polygon test on a coordinate that marks where a PEOPLE is found, not a settlement —
|
||||
* 108 groups even share a coordinate with another group. Near a border that point can fall on
|
||||
* point-in-polygon test on a coordinate that marks a PEOPLE group, not a settlement, and whose position
|
||||
* is approximate — 108 groups even share a coordinate with another group. Near a border that point can fall on
|
||||
* the wrong side. So every row carries `approximate: true`, the unassigned are COUNTED rather
|
||||
* than dropped, and no surface may present a province as a fact about a group.
|
||||
* Measured: 9 of 855 Indian groups and 2 of 154 Chinese ones land outside every polygon.
|
||||
|
|
@ -313,8 +313,8 @@ if (!assigned || assigned < 900) {
|
|||
grain: 'one row per PLACED people group; the four that fall outside every polygon are in the aggregate file\'s `unassigned`',
|
||||
rows: assignments.length,
|
||||
agrees_with: 'gospel-by-province.json — per-province counts and the assigned total are reconciled at build time, and neither file is written if they differ',
|
||||
approximate: 'Every row is approximate. The coordinate marks where a PEOPLE is found, not a ' +
|
||||
'settlement, and near a border it can fall on the wrong side. No surface may present a ' +
|
||||
approximate: 'Every row is approximate. The coordinate marks a PEOPLE group, not a ' +
|
||||
'settlement; its position is approximate, and near a border it can fall on the wrong side. No surface may present a ' +
|
||||
'province as a fact about a group.',
|
||||
},
|
||||
columns: [
|
||||
|
|
@ -373,8 +373,8 @@ if (!assigned || assigned < 900) {
|
|||
'It is a NEIGHBOUR, not a recommendation: whether a group actually speaks it is a ' +
|
||||
'question for someone on the ground.',
|
||||
approximate: 'A province is assigned by point-in-polygon on the group\'s coordinate, and ' +
|
||||
'that coordinate marks where a PEOPLE is found, not a settlement — 108 groups across the ' +
|
||||
'roster even share a coordinate with another group. Near a border the point can fall on ' +
|
||||
'that coordinate marks a PEOPLE group, not a settlement; its position is approximate, and 108 ' +
|
||||
'groups across the roster even share a coordinate with another group. Near a border the point can fall on ' +
|
||||
'the wrong side. Every row is flagged approximate and the unassigned are counted, never ' +
|
||||
'dropped. No surface may present a province as a fact about a group.',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<script setup>
|
||||
/**
|
||||
* cities-list.vue — KEY CITIES: where the unengaged people groups actually live.
|
||||
* cities-list.vue — KEY CITIES: clusters of unengaged people-group pins.
|
||||
*
|
||||
* The population centres derived from people-group locations, grouped by region,
|
||||
* each showing which groups are there and what languages they speak.
|
||||
* Clusters of people-group pins, grouped by region, each showing which groups'
|
||||
* pins fall in it and what languages they speak.
|
||||
*
|
||||
* ═══ WHAT A "KEY CITY" IS HERE — READ THIS BEFORE TRUSTING A ROW ════════════
|
||||
* A row is a CLUSTER OF PEOPLE-GROUP CENTROIDS within ~25km, not a municipal
|
||||
* A row is a CLUSTER OF PEOPLE-GROUP PINS within ~25km, not a municipal
|
||||
* boundary. The distinction is the integrity of the whole dataset.
|
||||
*
|
||||
* The pins are people-group centroids, not settlements — measured: India's 855
|
||||
* Each pin marks a people group, not a settlement — measured: India's 855
|
||||
* pins resolve to 625 distinct ~11km cells. So clustering them into "cities" and
|
||||
* naming those clusters ourselves would MANUFACTURE placenames, and a fabricated
|
||||
* placename in a persecution-sensitive dataset is the dangerous kind of wrong.
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@
|
|||
* geojson in the bundle would block first paint)
|
||||
*
|
||||
* ═══ WHAT A POINT IS, AND WHAT IT IS NOT ═══════════════════════════════════
|
||||
* A point is a CLUSTER OF PEOPLE-GROUP CENTROIDS within ~25km — not a municipal
|
||||
* boundary. Pins mark where a group is centred, not a settlement.
|
||||
* A point is a CLUSTER OF PEOPLE-GROUP PINS within ~25km — not a municipal
|
||||
* boundary. Each pin marks a people group, not a settlement.
|
||||
*
|
||||
* NAMED points were resolved by a cited gazetteer (Nominatim/OSM). UNNAMED ones
|
||||
* show as "unnamed location" with their coordinates rather than an invented
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"meta": {
|
||||
"title": "Key Cities / Location Clusters",
|
||||
"grain": "one row per ~25km location cluster",
|
||||
"caveat": "A cluster is people-group CENTROIDS, not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"caveat": "A cluster is people-group PINS (each marks a people group, not a town), not a settlement. place_name comes from a cited gazetteer (Nominatim/OSM, ODbL); an unnamed row is honest, not missing.",
|
||||
"internet_caveat": "Internet figures are NATIONAL. Per-city connectivity is not publicly available at this scale."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "Languages per City x Resources",
|
||||
"grain": "one row per location cluster x language spoken there",
|
||||
"why": "399 of 1,646 clusters are multi-language — a city-level bridge can exist that a country average hides.",
|
||||
"place_caveat": "A location is a cluster of people-group centroids within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"place_caveat": "A location is a cluster of people-group pins within ~25km, NOT a municipal boundary. An empty place_name is an unnamed cluster, never an invented one.",
|
||||
"resolution_caveat": "resolved:false means the language LABEL did not match an ISO 639-3 record here — its resource status is UNKNOWN, not absent.",
|
||||
"pools_caveat": "Resource status is from eBible + JESUS Film only. GRN and FCBH audio are key-blocked."
|
||||
},
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue