Phones: production maps not built for them stay closed and say why; experimental ones open with a notice

This commit is contained in:
Doxa Maps 2026-09-28 17:32:57 +00:00
parent 3b5a86aaef
commit 7ea66b21a2
4 changed files with 167 additions and 25 deletions

View file

@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* check-mobile-gating.mjs — the SPA's device rules stay on the right shelves (DRV1650-MOBILE-GATING).
*
* DESKTOP_ONLY production rows BLOCKED on a narrow screen (disabled + "desktop only" + a message)
* MOBILE_NOTICE experimental rows that OPEN on a narrow screen, with a "not built for mobile" notice
*
* Headless: reads plugin-designs/doxa-maps-spa/doxa-maps-spa.vue and src/strings.js. It fails if:
* · a DESKTOP_ONLY id is not a curated (production) tab, or a MOBILE_NOTICE id is not an Experimental
* bundle (curated, excluded, or no plugin folder), or an id is in both sets;
* · the predicates stop reading their set and the narrow flag, or the pane notice stops rendering;
* · any locale lacks mobileNotBuilt / mobileNotice / dismiss / desktopNotice, or desktopNotice
* stops naming the map ({label}), which it must now that more than one map can be desktop-only.
* `--self-test` proves every rule can fail on a planted bad case before trusting a pass.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SPA = join(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue')
const STRINGS = join(ROOT, 'plugin-designs/doxa-maps-spa/src/strings.js')
const KEYS = ['mobileNotBuilt', 'mobileNotice', 'dismiss', 'desktopNotice']
const setOf = (src, name) => {
const m = src.match(new RegExp(`const ${name} = new Set\\(\\[([^\\]]*)\\]\\)`))
return m ? new Set([...m[1].matchAll(/'([a-z0-9-]+)'/g)].map((x) => x[1])) : null
}
export function checkGating(src, locales, hasPlugin) {
const errs = []
const desk = setOf(src, 'DESKTOP_ONLY'), mob = setOf(src, 'MOBILE_NOTICE'), notExp = setOf(src, 'NOT_EXPERIMENTAL')
if (!desk) errs.push('DESKTOP_ONLY set not found')
if (!mob) errs.push('MOBILE_NOTICE set not found')
if (!notExp) errs.push('NOT_EXPERIMENTAL set not found')
if (errs.length) return errs
const curated = new Set([...src.matchAll(/bundle: '([a-z0-9-]+)'/g)].map((x) => x[1]))
for (const id of desk) if (!curated.has(id)) errs.push(`DESKTOP_ONLY "${id}" is not a curated production tab`)
for (const id of mob) {
if (curated.has(id)) errs.push(`MOBILE_NOTICE "${id}" is a curated production tab (production maps are blocked, not warned)`)
if (notExp.has(id)) errs.push(`MOBILE_NOTICE "${id}" is excluded from the Experimental shelf`)
if (!hasPlugin(id)) errs.push(`MOBILE_NOTICE "${id}" has no plugin-designs/${id} folder`)
if (desk.has(id)) errs.push(`"${id}" is in BOTH sets`)
}
if (!/isDesktopOnly\(id\)\s*\{\s*return DESKTOP_ONLY\.has\(id\) && this\.narrow/.test(src)) errs.push('isDesktopOnly no longer reads DESKTOP_ONLY + narrow')
if (!/showsMobileNotice\(id\)\s*\{\s*return MOBILE_NOTICE\.has\(id\) && this\.narrow/.test(src)) errs.push('showsMobileNotice no longer reads MOBILE_NOTICE + narrow')
if (!/v-if="showsMobileNotice\(t\.id\)"[^>]*role="status"/.test(src)) errs.push('the pane notice (showsMobileNotice, role=status) is not rendered')
for (const [loc, t] of Object.entries(locales || {})) {
for (const k of KEYS) if (!t || !String(t[k] || '').trim()) errs.push(`strings ${loc}: missing ${k}`)
if (t && t.desktopNotice && !t.desktopNotice.includes('{label}')) errs.push(`strings ${loc}: desktopNotice does not name the map ({label})`)
}
if (!locales || Object.keys(locales).length < 11) errs.push(`strings: ${Object.keys(locales || {}).length} locales, expected 11`)
return errs
}
async function loadLocales() {
const mod = await import(pathToFileURL(STRINGS).href)
return Object.values(mod).find((v) => v && typeof v === 'object' && v.en) || {}
}
if (process.argv.includes('--self-test')) {
const src = readFileSync(SPA, 'utf8'), locales = await loadLocales()
const yes = () => true
const cases = [
['a clean tree passes', src, locales, yes, 0],
['a curated id in MOBILE_NOTICE', src.replace("new Set(['gospel-internet'])", "new Set(['gospel-internet', 'dashboard-plus-grid'])"), locales, yes, 1],
['an experimental id in DESKTOP_ONLY', src.replace("new Set(['dashboard-plus-grid'])", "new Set(['dashboard-plus-grid', 'gospel-internet'])"), locales, yes, 1],
['a MOBILE_NOTICE id with no plugin', src, locales, () => false, 1],
['the predicate stops reading narrow', src.replace('return DESKTOP_ONLY.has(id) && this.narrow', 'return DESKTOP_ONLY.has(id)'), locales, yes, 1],
['the pane notice is removed', src.replace('v-if="showsMobileNotice(t.id)"', 'v-if="false"'), locales, yes, 1],
['a locale loses mobileNotice', src, { ...locales, ar: { ...locales.ar, mobileNotice: '' } }, yes, 1],
['desktopNotice stops naming the map', src, { ...locales, zh: { ...locales.zh, desktopNotice: '仅支持桌面端' } }, yes, 1],
]
let ok = true
for (const [name, s, l, hp, want] of cases) {
const n = checkGating(s, l, hp).length
const pass = want === 0 ? n === 0 : n >= want
if (!pass) ok = false
console.log(` ${pass ? '✓' : '✗'} ${name} -> ${n} error(s) (want ${want === 0 ? 0 : '≥' + want})`)
}
console.log(ok ? 'MOBILE-GATING SELF-TEST: PASS — every rule proven able to fail' : 'MOBILE-GATING SELF-TEST: FAIL')
process.exit(ok ? 0 : 1)
}
const src = readFileSync(SPA, 'utf8')
const errs = checkGating(src, await loadLocales(), (id) => existsSync(join(ROOT, 'plugin-designs', id)))
if (errs.length) {
console.error('MOBILE-GATING: FAIL\n' + errs.map((e) => ' ✗ ' + e).join('\n'))
process.exit(1)
}
console.log(`MOBILE-GATING: PASS — desktop-only: ${[...setOf(src, 'DESKTOP_ONLY')].join(', ')} · mobile notice: ${[...setOf(src, 'MOBILE_NOTICE')].join(', ')} · 11 locales`)

View file

@ -19,6 +19,7 @@
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
"capability-exemplars": "node internal/generate-capability-exemplars.mjs",
"check:a11y-button-names": "node internal/check-a11y-button-names.mjs",
"check:mobile-gating": "node internal/check-mobile-gating.mjs",
"check:app-start-page": "node internal/set-app-start-page.mjs --check",
"check:approx-pin-note": "node internal/check-approx-pin-note.mjs",
"check:auto-engine": "node internal/check-auto-engine.mjs",
@ -285,7 +286,7 @@
"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:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels",
"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:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist 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",
"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:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist 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",
"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",
@ -338,6 +339,7 @@
"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.",
"capability-exemplars": "regenerate documentation/reference-capability-exemplars.md — the DERIVED answer to \"which bundle is the capability-N example?\", a question answered WRONGLY in three separate artifacts (a gate, the README, the /plugin-designer skill).",
"check:a11y-button-names": "every VISIBLE button must have an accessible name (aria-label / aria-labelledby / visible text — a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
"check:mobile-gating": "the SPA device rules stay on their shelves: DESKTOP_ONLY ids are curated production tabs (blocked on a narrow screen, disabled + a message naming the map), MOBILE_NOTICE ids are Experimental bundles (they open on a narrow screen with a dismissible not-built-for-mobile notice), the predicates read their set + narrow, and all 11 locales carry the four strings. Headless; --self-test proves each rule can fail.",
"check:approx-pin-note": "people-group pins say they are localities, not towns or boundaries, in ONE shared wording (library/components/legends/ApproxPinNote.vue): the colocation index finds groups sharing a coordinate, the note names them only for a shared point, never prints a raw coordinate, and renders from each locale's catalogue. Headless.",
"check:auto-engine": "BROWSER. The default engine is auto: on one map leaf, no token draws with MapLibre and makes zero api.mapbox.com requests, a token draws with Mapbox, and an explicit provider wins. Under the fleet browser policy it is a rega run, not part of gates:post.",
"check:backlog-refs": "a card may not depend on a card that does not exist. A dangling depends_on can never be done, so the card can never become ready and nothing says so — it reads as ordinary blocked work forever. FAILS only for depends_on on an OPEN card; a dangling parent or a stale link on a done card is REPORTED, because failing on filing debt teaches people to delete the reference rather than restore the card. Reads all three phase backlogs — references legitimately cross phases. SKIPs when _qa-cycle is absent.",

View file

@ -129,6 +129,10 @@
v-show="i === activeIndex" :hidden="i !== activeIndex">
<template v-if="!t.heavy || i === activeIndex">
<div v-if="showsMobileNotice(t.id)" class="dms-mobile-notice" role="status">
<span>{{ tr('mobileNotice') }}</span>
<button type="button" class="dms-mobile-notice-x" :aria-label="tr('dismiss')" @click="dismissMobileNotice(t.id)">×</button>
</div>
<div class="dms-slot" :ref="el => setSlot(i, el)"></div>
<div v-if="t.state === 'loading'" class="dms-hint">{{ tr('loading', { label: tabLabel(t) }) }}</div>
<div v-else-if="t.state === 'error'" class="dms-hint err">
@ -224,7 +228,7 @@ const NAV_MODEL = [
{ key: 'dashboards', label: 'Dashboards', i18n: 'sec.dashboards', icon: 'chart',
tabs: [
// 1. Big Picture — desktop default. Present on EVERY width (the device rule is "same tab
// order everywhere"), but DISABLED on mobile rather than hidden — see isBigPictureDisabled /
// order everywhere"), but DISABLED on mobile rather than hidden — see isDesktopOnly /
// onSelect's intercept and the `disabled`-row note above navGroups. Clicking it on mobile
// shows a message instead of switching; it never disappears from the rail.
{ id: 'dashboard-plus-grid', icon: 'chart' },
@ -295,6 +299,16 @@ const NAV_MODEL = [
// - people-groups-grid / research-dashboard / big-picture: the PARTS that are already composed
// into the ONE `dashboard-plus-grid` Big Picture tab — surfacing them separately is the exact
// redundancy the Driver removed ("we don't need multiple dashboards").
// ── DEVICE RULES (Driver, 2026-09-28 16:50Z: DRV1650-MOBILE-GATING) ─────────────────────────
// PRODUCTION maps that are not mobile-ready are BLOCKED on a narrow screen: the row stays in the rail,
// disabled with a "desktop only" badge, and a tap explains why instead of mounting (Big Picture was the
// first, and still works exactly that way). EXPERIMENTAL maps not built for mobile still OPEN on a
// narrow screen, with a "not built for mobile" badge and a dismissible notice in the pane: an
// experiment should be reachable, and honest about where it works. Add an id here, not a width test
// anywhere else; check:mobile-gating holds the two sets to their shelves.
const DESKTOP_ONLY = new Set(['dashboard-plus-grid'])
const MOBILE_NOTICE = new Set(['gospel-internet'])
const NOT_EXPERIMENTAL = new Set([
'doxa-maps-spa',
'plugin-studio',
@ -381,7 +395,7 @@ const TAB_DEFS = [
// wrong choice of saying that we should swap elements based on mobile and desktop... It'd be
// easier just to have two separate capability-4 plugins." So there are two cap-4 composites,
// neither aware of the other — this one and 'dashboard-plus-grid' above — and the shell decides
// which is AUTO-SELECTED by width (see firstVisibleIndex/isBigPictureDisabled), never which is
// which is AUTO-SELECTED by width (see firstVisibleIndex/isDesktopOnly), never which is
// SHOWN: per the device rule both rows are always in the rail. Both composites stay independently
// embeddable, which a self-swapping element could not be (an embedder at a fixed size could never
// ask for one).
@ -783,6 +797,7 @@ export default defineComponent({
// so two quick clicks don't fight over when it disappears.
disabledNotice: '', _noticeTimer: null,
helpOpen: false,
mobileNoticeDismissed: {},
// Set when EVERY manifest candidate failed. The manifest carries each tab's scriptUrl, so
// without it no src-composed tab can mount; this turns that into a visible pane message
// instead of an empty grey rectangle. See loadManifest().
@ -868,7 +883,7 @@ export default defineComponent({
// the 2nd tab by default on mobile and disable the 1st tab." That is why there is NO width
// filter on rows here any more (the old `onlyWhen:'desktop'|'mobile'` hide-a-row mechanism is
// gone — every row is in the rail on every width, same order, always). The ONLY width-driven
// thing left is which row is DISABLED (Big Picture, on mobile — see isBigPictureDisabled) and
// thing left is which row is DISABLED (Big Picture, on mobile — see isDesktopOnly) and
// which row auto-opens (see firstVisibleIndex) — both presentation, neither touches NAV_MODEL.
navGroups() {
const byId = Object.fromEntries(this.tabs.map(t => [t.id, t]))
@ -887,10 +902,10 @@ export default defineComponent({
// Big Picture is DISABLED (not hidden) on mobile — the one row-level exception the
// device rule requires. `disabled`/`badge` are read by THIS shell's onSelect intercept
// and CSS below; VerticalTabsLayout itself does not know about `disabled` (it has no
// such concept — see the comment on isBigPictureDisabled), so the row stays clickable
// such concept — see the comment on isDesktopOnly), so the row stays clickable
// at the layout level and this shell is what turns that click into a message instead
// of a mount.
const disabled = this.isBigPictureDisabled(t.id)
const disabled = this.isDesktopOnly(t.id)
return {
key: t.id, label: this.tabLabel(t), sub: this.tabSub(t), icon: r.icon,
badge: disabled ? this.tr('desktopOnly') : r.badge, disabled
@ -906,7 +921,8 @@ export default defineComponent({
// routing it through `seen` means even a mis-declared bundle can only ever paint one row.
rows = rows.concat(dedupeRows(
this.tabs.filter(t => t.discovered)
.map(t => ({ key: t.id, label: this.tabLabel(t), sub: this.tabSub(t), icon: 'flask', badge: this.tr('lab') })),
.map(t => ({ key: t.id, label: this.tabLabel(t), sub: this.tabSub(t), icon: 'flask',
badge: MOBILE_NOTICE.has(t.id) && this.narrow ? this.tr('mobileNotBuilt') : this.tr('lab') })),
seen))
}
return { key: g.key, label: g.i18n ? this.tr(g.i18n) : g.label, icon: g.icon, folder: !!g.folder, open: g.open, rows }
@ -1048,7 +1064,7 @@ export default defineComponent({
// desktop→mobile). Every row stays IN the rail on both widths per the device rule — nothing
// is removed any more — but a disabled active tab must not be left mounted/highlighted with
// no way to reach it, so snap to the default row for the new width.
if (this.isBigPictureDisabled(this.activeKey)) this.select(this.firstVisibleIndex())
if (this.isDesktopOnly(this.activeKey)) this.select(this.firstVisibleIndex())
}
// addEventListener is the modern API; addListener is the deprecated fallback some webviews
// still need. Try both rather than assume — a missed listener means the swap silently never
@ -1061,8 +1077,15 @@ export default defineComponent({
// predicate (not a hardcoded width filter in navGroups) so the ONE place that knows "Big Picture"
// means 'dashboard-plus-grid' is this method — navGroups, onSelect and watchWidth all call it
// rather than each re-encoding the id + the width test.
isBigPictureDisabled(id) {
return id === 'dashboard-plus-grid' && this.narrow
isDesktopOnly(id) {
return DESKTOP_ONLY.has(id) && this.narrow
},
// An experimental map that opens on a phone but warns first (MOBILE_NOTICE), until dismissed.
showsMobileNotice(id) {
return MOBILE_NOTICE.has(id) && this.narrow && !this.mobileNoticeDismissed[id]
},
dismissMobileNotice(id) {
this.mobileNoticeDismissed = { ...this.mobileNoticeDismissed, [id]: true }
},
// Index (into this.tabs) of the row the rail should auto-open at the CURRENT width. Desktop opens
// tab 1 (Big Picture); mobile opens tab 2 (Simple Research) because tab 1 is disabled there — the
@ -1188,7 +1211,7 @@ export default defineComponent({
const want = this.requestedTab()
const w = want ? this.tabs.findIndex(t => t.id === want) : -1
const i = this.tabs.findIndex(t => t.id === activeId)
if (w >= 0 && !this.isBigPictureDisabled(want) && !this._honouredTab) this.select(w)
if (w >= 0 && !this.isDesktopOnly(want) && !this._honouredTab) this.select(w)
else this.select(i >= 0 ? i : this.firstVisibleIndex())
this._honouredTab = true
},
@ -1205,7 +1228,7 @@ export default defineComponent({
const want = this.requestedTab()
const i = want ? this.tabs.findIndex(t => t.id === want) : -1
if (i >= 0) this._honouredTab = true
return (i >= 0 && !this.isBigPictureDisabled(want)) ? i : this.firstVisibleIndex()
return (i >= 0 && !this.isDesktopOnly(want)) ? i : this.firstVisibleIndex()
},
// Switch UI language. Persist the choice and let the `effectiveLocale` WATCHER apply it — it
// re-sets profile-config on the active tab's element, generically, with no per-tab special case.
@ -1232,8 +1255,9 @@ export default defineComponent({
// the `disabled`-badge styling below), never removed — exactly the device rule's "disable, don't
// hide."
onSelect(key) {
if (this.isBigPictureDisabled(key)) {
this.disabledNotice = this.tr('desktopNotice')
if (this.isDesktopOnly(key)) {
const t = this.tabs.find(x => x.id === key)
this.disabledNotice = this.tr('desktopNotice', { label: t ? this.tabLabel(t) : key })
clearTimeout(this._noticeTimer)
this._noticeTimer = setTimeout(() => { this.disabledNotice = '' }, 4000)
return
@ -1543,6 +1567,20 @@ export default defineComponent({
padding: 10px 16px; box-shadow: 0 8px 24px rgba(0,0,0,.12);
}
/* "Not built for mobile" (MOBILE_NOTICE): sticky at the top of the pane's own scroller, so it stays in
view over the map and never covers the rail. Dismissible per map for the session. */
.dms-mobile-notice {
position: sticky; top: 0; z-index: 30; display: flex; align-items: flex-start; gap: 8px;
padding: 10px 12px; font-size: 13px; line-height: 1.4; color: #3b463d;
background: #fff8e6; border-bottom: 1px solid #f0d58c;
}
.dms-mobile-notice span { flex: 1; }
.dms-mobile-notice-x {
flex: none; min-width: 32px; min-height: 32px; border: 0; border-radius: 6px;
background: transparent; color: inherit; font-size: 18px; line-height: 1; cursor: pointer;
}
.dms-mobile-notice-x:focus-visible { outline: 2px solid #3b463d; outline-offset: 1px; }
/* #brand logo mark (left of the shell title). */
.dms-logo { display: inline-flex; align-items: center; }
.dms-logo svg { display: block; }

View file

@ -28,12 +28,13 @@ export const STRINGS = {
navLabel: 'Research maps', meta: '{n} maps', loading: 'Loading {label}…', loadFailed: 'Could not load {label}.',
manifestHint: 'This tab is composed from manifest.json — build the tree, or serve the built tree, to load it.',
close: 'Close', help: 'About this map', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
mobileNotBuilt: 'not built for mobile', mobileNotice: 'This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.', dismiss: 'Dismiss', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/en/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Doxa Vision Card",
desktopNotice: 'Big Picture only supports desktop — open this page on a desktop to use it.'
desktopNotice: '{label} only supports desktop — open this page on a desktop to use it.'
},
es: {
'sec.dashboards': 'Paneles', 'sec.prayer': 'Movilización de oración', 'sec.gospel': 'Investigación del Evangelio', 'sec.experimental': 'Experimental',
@ -48,12 +49,13 @@ export const STRINGS = {
navLabel: 'Mapas de investigación', meta: '{n} mapas', loading: 'Cargando {label}…', loadFailed: 'No se pudo cargar {label}.',
manifestHint: 'Esta pestaña se compone a partir de manifest.json: compila el árbol o sirve el árbol compilado para cargarla.',
close: 'Cerrar', help: 'Acerca de este mapa', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
mobileNotBuilt: 'no diseñado para móvil', mobileNotice: 'Este mapa experimental está pensado para pantallas grandes. Se abre aquí, pero algunas partes pueden no caber o no funcionar bien en un teléfono.', dismiss: 'Cerrar', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/es/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
desktopNotice: 'Panorama general solo funciona en escritorio: abre esta página en una computadora para usarlo.'
desktopNotice: '{label} solo funciona en escritorio: abra esta página en un ordenador para usarlo.'
},
fr: {
'sec.dashboards': 'Tableaux de bord', 'sec.prayer': 'Mobilisation à la prière', 'sec.gospel': "Recherche sur l'Évangile", 'sec.experimental': 'Expérimental',
@ -68,12 +70,13 @@ export const STRINGS = {
navLabel: 'Cartes de recherche', meta: '{n} cartes', loading: 'Chargement de {label}…', loadFailed: 'Impossible de charger {label}.',
manifestHint: "Cet onglet est composé à partir de manifest.json : construisez l'arborescence ou servez l'arborescence construite pour le charger.",
close: 'Fermer', help: 'À propos de cette carte', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
mobileNotBuilt: 'pas conçu pour mobile', mobileNotice: 'Cette carte expérimentale est conçue pour les grands écrans. Elle s’ouvre ici, mais certaines parties peuvent mal s’afficher ou mal fonctionner sur un téléphone.', dismiss: 'Fermer', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/fr/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Carte de vision Doxa",
desktopNotice: "Vue d'ensemble ne fonctionne que sur ordinateur : ouvrez cette page sur un ordinateur pour l'utiliser."
desktopNotice: '{label} ne fonctionne que sur ordinateur : ouvrez cette page sur un ordinateur pour l’utiliser.'
},
de: {
'sec.dashboards': 'Dashboards', 'sec.prayer': 'Gebetsmobilisierung', 'sec.gospel': 'Evangeliumsforschung', 'sec.experimental': 'Experimentell',
@ -88,12 +91,13 @@ export const STRINGS = {
navLabel: 'Forschungskarten', meta: '{n} Karten', loading: '{label} wird geladen…', loadFailed: '{label} konnte nicht geladen werden.',
manifestHint: 'Dieser Tab wird aus manifest.json zusammengesetzt – bauen Sie den Baum oder stellen Sie den gebauten Baum bereit, um ihn zu laden.',
close: 'Schließen', help: 'Über diese Karte', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
mobileNotBuilt: 'nicht für Mobilgeräte', mobileNotice: 'Diese experimentelle Karte ist für große Bildschirme gebaut. Sie öffnet sich hier, aber Teile passen oder funktionieren auf einem Telefon womöglich nicht gut.', dismiss: 'Schließen', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/de/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Doxa-Visionskarte",
desktopNotice: 'Gesamtbild funktioniert nur am Desktop – öffnen Sie diese Seite an einem Computer, um es zu nutzen.'
desktopNotice: '{label} funktioniert nur am Desktop – öffnen Sie diese Seite an einem Computer.'
},
pt: {
'sec.dashboards': 'Painéis', 'sec.prayer': 'Mobilização de oração', 'sec.gospel': 'Pesquisa do Evangelho', 'sec.experimental': 'Experimental',
@ -108,12 +112,13 @@ export const STRINGS = {
navLabel: 'Mapas de pesquisa', meta: '{n} mapas', loading: 'Carregando {label}…', loadFailed: 'Não foi possível carregar {label}.',
manifestHint: 'Esta aba é composta a partir do manifest.json — compile a árvore ou sirva a árvore compilada para carregá-la.',
close: 'Fechar', help: 'Sobre este mapa', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
mobileNotBuilt: 'não feito para celular', mobileNotice: 'Este mapa experimental foi feito para telas maiores. Ele abre aqui, mas algumas partes podem não caber ou não funcionar bem no celular.', dismiss: 'Fechar', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/pt/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Cartão de visão Doxa",
desktopNotice: 'Visão geral só funciona no computador — abra esta página em um computador para usá-la.'
desktopNotice: '{label} só funciona no computador — abra esta página num computador para usá-lo.'
},
it: {
'sec.dashboards': 'Dashboard', 'sec.prayer': 'Mobilitazione alla preghiera', 'sec.gospel': 'Ricerca sul Vangelo', 'sec.experimental': 'Sperimentale',
@ -130,12 +135,13 @@ export const STRINGS = {
navLabel: 'Mappe di ricerca', meta: '{n} mappe', loading: 'Caricamento di {label}…', loadFailed: 'Impossibile caricare {label}.',
manifestHint: "Questa scheda è composta da manifest.json: compila l'albero o pubblica l'albero compilato per caricarla.",
close: 'Chiudi', help: 'Informazioni su questa mappa', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
mobileNotBuilt: 'non pensato per mobile', mobileNotice: 'Questa mappa sperimentale è pensata per schermi grandi. Si apre qui, ma alcune parti potrebbero non entrare o non funzionare bene su un telefono.', dismiss: 'Chiudi', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/it/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Scheda visione Doxa",
desktopNotice: "Quadro d'insieme funziona solo su desktop: apri questa pagina su un computer per usarlo."
desktopNotice: '{label} funziona solo su desktop: apri questa pagina da un computer per usarlo.'
},
ro: {
'sec.dashboards': 'Panouri', 'sec.prayer': 'Mobilizare la rugăciune', 'sec.gospel': 'Cercetarea Evangheliei', 'sec.experimental': 'Experimental',
@ -150,12 +156,13 @@ export const STRINGS = {
navLabel: 'Hărți de cercetare', meta: '{n} hărți', loading: 'Se încarcă {label}…', loadFailed: '{label} nu a putut fi încărcat.',
manifestHint: 'Această filă este compusă din manifest.json — construiți arborele sau serviți arborele construit pentru a o încărca.',
close: 'Închide', help: 'Despre această hartă', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
mobileNotBuilt: 'nu e făcut pentru mobil', mobileNotice: 'Această hartă experimentală este făcută pentru ecrane mari. Se deschide aici, dar unele părți s-ar putea să nu încapă sau să nu funcționeze bine pe telefon.', dismiss: 'Închide', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ro/studio.json
// (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",
// 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", 'tab.doxa-vision-card': "Card de viziune Doxa",
desktopNotice: 'Imaginea de ansamblu funcționează doar pe desktop — deschideți această pagină pe un computer pentru a o folosi.'
desktopNotice: '{label} funcționează doar pe desktop — deschideți pagina pe un computer pentru a-l folosi.'
},
ru: {
'sec.dashboards': 'Панели', 'sec.prayer': 'Молитвенная мобилизация', 'sec.gospel': 'Исследование Евангелия', 'sec.experimental': 'Экспериментальное',
@ -170,12 +177,13 @@ export const STRINGS = {
navLabel: 'Исследовательские карты', meta: 'Карт: {n}', loading: 'Загрузка: {label}…', loadFailed: 'Не удалось загрузить: {label}.',
manifestHint: 'Эта вкладка собирается из manifest.json — соберите дерево или разместите собранное дерево, чтобы загрузить её.',
close: 'Закрыть', help: 'Об этой карте', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
mobileNotBuilt: 'не для телефона', mobileNotice: 'Эта экспериментальная карта рассчитана на большие экраны. Она откроется, но на телефоне часть элементов может не поместиться или работать плохо.', dismiss: 'Закрыть', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ru/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Показать или скрыть список", openViews: "Открыть список представлений", expandList: "Развернуть список", collapseList: "Свернуть список", collapse: "Свернуть",
// 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': "Плакаты по странам", 'tab.doxa-vision-card': "Карточка видения Doxa",
desktopNotice: '«Общая картина» работает только на компьютере — откройте эту страницу на компьютере.'
desktopNotice: '{label} работает только на компьютере — откройте эту страницу на компьютере.'
},
ar: {
'sec.dashboards': 'لوحات المعلومات', 'sec.prayer': 'التعبئة للصلاة', 'sec.gospel': 'أبحاث الإنجيل', 'sec.experimental': 'تجريبي',
@ -190,12 +198,13 @@ export const STRINGS = {
navLabel: 'خرائط البحث', meta: 'عدد الخرائط: {n}', loading: 'جارٍ تحميل {label}…', loadFailed: 'تعذّر تحميل {label}.',
manifestHint: 'تُبنى علامة التبويب هذه من manifest.json — ابنِ الشجرة أو قدّم الشجرة المبنية لتحميلها.',
close: 'إغلاق', help: 'عن هذه الخريطة', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
mobileNotBuilt: 'غير مُعدّ للهاتف', mobileNotice: 'هذه الخريطة التجريبية مُعدّة للشاشات الكبيرة. تُفتح هنا، لكن بعض أجزائها قد لا يتّسع أو لا يعمل جيدًا على الهاتف.', dismiss: 'إغلاق', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ar/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "إظهار القائمة أو إخفاؤها", openViews: "فتح قائمة العروض", expandList: "توسيع القائمة", collapseList: "طي القائمة", collapse: "طي",
// 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': "ملصقات الدول", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
desktopNotice: 'تعمل «الصورة الكاملة» على الحاسوب فقط — افتح هذه الصفحة على حاسوب لاستخدامها.'
desktopNotice: '{label} يعمل على الحاسوب فقط — افتح هذه الصفحة على حاسوب لاستخدامه.'
},
hi: {
'sec.dashboards': 'डैशबोर्ड', 'sec.prayer': 'प्रार्थना संचालन', 'sec.gospel': 'सुसमाचार शोध', 'sec.experimental': 'प्रायोगिक',
@ -210,12 +219,13 @@ export const STRINGS = {
navLabel: 'शोध मानचित्र', meta: '{n} मानचित्र', loading: '{label} लोड हो रहा है…', loadFailed: '{label} लोड नहीं हो सका।',
manifestHint: 'यह टैब manifest.json से बनता है — इसे लोड करने के लिए ट्री बनाएँ या बना हुआ ट्री सर्व करें।',
close: 'बंद करें', help: 'इस नक्शे के बारे में', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
mobileNotBuilt: 'मोबाइल के लिए नहीं बना', mobileNotice: 'यह प्रायोगिक नक्शा बड़ी स्क्रीन के लिए बना है। यह यहाँ खुलता है, पर फ़ोन पर इसके कुछ हिस्से ठीक से न दिखें या न चलें।', dismiss: 'बंद करें', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/hi/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "सूची दिखाएँ या छिपाएँ", openViews: "दृश्यों की सूची खोलें", expandList: "सूची फैलाएँ", collapseList: "सूची संक्षिप्त करें", collapse: "संक्षिप्त करें",
// 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': "देश पोस्टर", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
desktopNotice: 'संपूर्ण चित्र केवल डेस्कटॉप पर काम करता है — इसे उपयोग करने के लिए यह पेज कंप्यूटर पर खोलें।'
desktopNotice: '{label} केवल डेस्कटॉप पर चलता है — इसे इस्तेमाल करने के लिए यह पेज कंप्यूटर पर खोलें।'
},
zh: {
'sec.dashboards': '仪表板', 'sec.prayer': '祷告动员', 'sec.gospel': '福音研究', 'sec.experimental': '实验性',
@ -230,12 +240,13 @@ export const STRINGS = {
navLabel: '研究地图', meta: '{n} 张地图', loading: '正在加载{label}…', loadFailed: '无法加载{label}。',
manifestHint: '此标签页由 manifest.json 组成——请构建目录树或提供已构建的目录树以加载它。',
close: '关闭', help: '关于此地图', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
mobileNotBuilt: '未针对手机设计', mobileNotice: '这张实验性地图为大屏幕设计。它可以在这里打开,但部分内容在手机上可能显示不全或无法正常使用。', dismiss: '关闭', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/zh/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "显示或隐藏列表", openViews: "打开视图列表", expandList: "展开列表", collapseList: "折叠列表", collapse: "折叠",
// 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': "国家海报", 'tab.doxa-vision-card': "Doxa 异象卡",
desktopNotice: '全景仅支持桌面设备——请在电脑上打开此页面使用。'
desktopNotice: '{label} 仅支持桌面端——请在电脑上打开此页面使用。'
}
}