A map explains itself: an info button, a one-time pointer and an example popup

MapInfo (library/components/info) answers "what can this map do for
you?" with real output the map makes, pre-generated so it opens at once.
The examples are an ordered list of kinds rather than a fixed set,
because maps make different things: an image (a poster), pages (a packet
that shows its first page and expands to every page), a deck (cards that
expand from a stack to a grid) and a table (a real excerpt of the
spreadsheet a map exports, with translatable headers).

The pointer at the button goes for good once that map's popup has been
opened and closed; a checkbox in the popup turns it off on every map.
Both are kept in localStorage behind a guard, so private browsing cannot
break the page. On a phone the popup takes the whole screen; Escape
closes it and focus returns to the button. The pointer's motion stops
under prefers-reduced-motion.

Its own words are a new 'mapInfo' area in all eleven locales. Each map's
lead, takeaway and note arrive as keys in that map's own area, resolved
at render so a live language switch re-translates them; the UPG-100
map's two sentences are added to upgMap in all eleven locales.
This commit is contained in:
Doxa Maps 2026-09-29 18:50:57 +00:00
parent 10be43d2b1
commit f293cf7e99
24 changed files with 603 additions and 11 deletions

View file

@ -42,6 +42,8 @@ and a change here can break other maps.
`import … from '@map/components/dashboards/DashboardFilterChips.vue'`
- **PinHint** — PinHint.vue — the conversion coach-mark (bottom-center of any map).
`import … from '@map/components/hints/PinHint.vue'`
- **MapInfo** — MapInfo.vue — a map's "what can this do for you?" button, one-time pointer and example popup.
`import … from '@map/components/info/MapInfo.vue'`
- **ApproxPinNote** — ApproxPinNote.vue — the ONE wording every map with people-group pins uses (APPROXPIN, 2026-09-26).
`import … from '@map/components/legends/ApproxPinNote.vue'`
- **LegendCollapseCaret** — LegendCollapseCaret.vue — SHARED (@map/components/legends/LegendCollapseCaret.vue).
@ -141,6 +143,8 @@ and a change here can break other maps.
`import … from '@map/composables/poster/usePosterLayout.js'`
- **printInPlace** — printInPlace.js — SHARED (@map/composables/printInPlace.js).
`import … from '@map/composables/printInPlace.js'`
- **retryFetch** — retryFetch.js — fetch(), with the same transient-blip retry the lazy components already get.
`import … from '@map/composables/retryFetch.js'`
- **retryableAsyncComponent** — retryableAsyncComponent.js — SHARED (@map/composables/retryableAsyncComponent.js).
`import … from '@map/composables/retryableAsyncComponent.js'`
- **useAvatarGenerator** — Avatar Generator Composable

View file

@ -0,0 +1,346 @@
<script setup>
/**
* MapInfo.vue — a map's "what can this do for you?" button, one-time pointer and example popup.
*
* The popup answers the question with REAL output the map makes.
*
* WHY: a map shown to someone with nobody beside them to explain it has to explain itself. The
* popup does that by showing what you walk away with (a poster, a prayer packet, a deck of cards, a
* spreadsheet), pre-generated, so it opens instantly instead of rendering for seconds.
*
* ★ EXAMPLES ARE A LIST, NOT A FIXED POSTER/PACKET/CARDS TRIPLE. Maps make different things: the
* UPG-100 and Regions maps make posters, packets and cards; Big Picture makes only a spreadsheet
* (its export is a CSV and it has no poster or packet anywhere). A fixed triple would promise a
* button a map does not have. Each item is one artefact, in the order given:
* { kind: 'image', src, full?, alt?, titleKey? } a poster or any single picture; `full` opens
* the full-size file in a new tab
* { kind: 'pages', pages: [src, …], titleKey? } the first page; expands to every page and back
* { kind: 'deck', cards: [src, …], titleKey? } a stacked deck; expands to every card and back
* { kind: 'table', columns: [..], rows: [[..]..], total?, titleKey? }
* a small real excerpt of a spreadsheet; `total`
* says how many rows the full export has. A
* column is a ready string or { key } — pass the
* table's own header keys so an Arabic reader does
* not get English headers. Number cells are
* formatted in the reader's locale.
* Relative srcs resolve against `assetsBase`, so the same list works in any tree.
*
* ★ THE POINTER GOES FOR GOOD, per map, once its popup has been opened AND closed (opening alone
* could be a mis-tap). The checkbox in the popup turns the pointer off on EVERY map. Both live in
* localStorage, read through a guard: private browsing and sandboxed frames throw on access, and a
* hint must never be the thing that breaks a page. The button itself always stays.
*
* ★ TEXT. The component's own words are in the 'mapInfo' catalogue area, in all eleven locales.
* The per-map words (lead, takeaway, note) belong to the map's owner and arrive as KEYS in the
* owner's own area, or as ready strings. Everything is resolved at render, never stored, so a live
* language switch re-translates it. A host without vue-i18n has only the page-global locale, which
* nothing observes; pass `locale` from a host that tracks it, and the popup also re-reads the
* global every time it opens.
*
* Usage (one per map, inside a positioned container):
* <MapInfo storage-key="upg-100" lead-key="…" :examples="[…]" :assets-base="assetsBase" />
*/
import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { useOptionalT } from '@map/composables/useOptionalT.js'
import { tStatic, getRuntimeLocale } from '@map/i18n/index.js'
const props = defineProps({
/** One per map. Keys the "this map's pointer has been seen" flag. */
storageKey: { type: String, required: true },
/** The map's own sentence: what it does for the reader. A catalogue key, or a ready string. */
leadKey: { type: String, default: '' },
lead: { type: String, default: '' },
/** What the reader takes away. Key or string, optional. */
takeawayKey: { type: String, default: '' },
takeaway: { type: String, default: '' },
/** A caveat under the examples (e.g. which countries are not supported yet). Key or string. */
noteKey: { type: String, default: '' },
note: { type: String, default: '' },
/** The artefacts, in display order. See the header for the four kinds. */
examples: { type: Array, default: () => [] },
/** Base for relative example srcs, normally the tree's assets folder. */
assetsBase: { type: String, default: '' },
/** Where the button sits in its container. */
placement: { type: String, default: 'top-right', validator: (v) => ['top-right', 'top-left'].includes(v) },
/** The host's current locale, when it tracks one. */
locale: { type: String, default: '' },
})
// ── locale and text ──────────────────────────────────────────────────────────────────────────────
// `opens` is read by every lookup, so opening the popup re-reads the page-global locale on hosts
// with no reactive one.
const opens = ref(0)
function lang() {
void opens.value
return String(props.locale || getRuntimeLocale() || 'en').split('-')[0].toLowerCase()
}
const t = useOptionalT('', { fallback: (q) => tStatic(q, lang()) })
const text = (key, str) => (key ? t(key) : str)
const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } }
const isRtl = () => lang() === 'ar'
// ── storage, guarded ─────────────────────────────────────────────────────────────────────────────
const SEEN = 'dxmaps.mapInfo.seen.'
const OFF = 'dxmaps.mapInfo.pointer'
const OFF_EVENT = 'dxmaps:mapinfo-pointer-off'
function readStore(k) { try { return window.localStorage.getItem(k) } catch { return null } }
function writeStore(k, v) { try { window.localStorage.setItem(k, v) } catch { /* private mode: in-memory only */ } }
const pointing = ref(false)
const pointerOff = ref(false)
function refreshPointer() {
pointerOff.value = readStore(OFF) === 'off'
pointing.value = !pointerOff.value && readStore(SEEN + props.storageKey) !== '1'
}
// Another map on the page (same document) or another tab (storage event) turned the pointer off.
function onPointerOff() { refreshPointer() }
function onStorage(e) { if (!e || e.key === OFF || e.key === SEEN + props.storageKey) refreshPointer() }
onMounted(() => {
refreshPointer()
window.addEventListener(OFF_EVENT, onPointerOff)
window.addEventListener('storage', onStorage)
})
onBeforeUnmount(() => {
window.removeEventListener(OFF_EVENT, onPointerOff)
window.removeEventListener('storage', onStorage)
})
function setPointerOff(off) {
pointerOff.value = off
writeStore(OFF, off ? 'off' : 'on')
pointing.value = !off && readStore(SEEN + props.storageKey) !== '1'
try { window.dispatchEvent(new CustomEvent(OFF_EVENT)) } catch { /* no window */ }
}
// ── open / close ─────────────────────────────────────────────────────────────────────────────────
const open = ref(false)
const btnEl = ref(null)
const closeEl = ref(null)
const expanded = reactive({})
const failed = reactive({})
async function show() {
opens.value++
open.value = true
await nextTick()
closeEl.value?.focus()
}
async function hide() {
open.value = false
writeStore(SEEN + props.storageKey, '1')
pointing.value = false
await nextTick()
btnEl.value?.focus()
}
function onKey(e) { if (e.key === 'Escape') { e.stopPropagation(); hide() } }
function toggle(i) { expanded[i] = !expanded[i] }
// ── examples ─────────────────────────────────────────────────────────────────────────────────────
function url(src) {
const s = String(src || '')
if (!s || /^(?:[a-z]+:|\/)/i.test(s) || !props.assetsBase) return s
return props.assetsBase.replace(/\/+$/, '') + '/' + s.replace(/^\.?\//, '')
}
const DEFAULT_TITLE = { image: 'mapInfo.poster', pages: 'mapInfo.packet', deck: 'mapInfo.cards', table: 'mapInfo.spreadsheet' }
function titleOf(ex) { return t(ex.titleKey || DEFAULT_TITLE[ex.kind] || 'mapInfo.example') }
const deckTop = (ex) => (ex.cards || []).slice(0, 5)
const colLabel = (c) => (c && typeof c === 'object' ? (c.key ? t(c.key) : String(c.label || '')) : String(c ?? ''))
useShadowStyles(`
.mi-btn{ position:absolute; z-index:20; top:var(--map-info-top, 12px); width:40px; height:40px;
border-radius:50%; border:1px solid rgba(0,0,0,.18); background:#fff; color:#1f2a44; cursor:pointer;
display:inline-flex; align-items:center; justify-content:center; padding:0;
box-shadow:0 2px 8px rgba(0,0,0,.18); font:inherit; }
.mi-btn:hover{ background:#f3f5fa; }
.mi-btn:focus-visible{ outline:3px solid #2563eb; outline-offset:2px; }
.mi-btn svg{ width:22px; height:22px; }
.mi-at-right{ right:var(--map-info-side, 12px); }
.mi-at-left{ left:var(--map-info-side, 12px); }
.mi-pointer{ position:absolute; z-index:20; top:calc(var(--map-info-top, 12px) + 4px);
display:flex; align-items:center; gap:6px; pointer-events:none; white-space:nowrap;
background:#1f2a44; color:#fff; font-size:13px; font-weight:600; line-height:1; padding:8px 12px;
border-radius:999px; box-shadow:0 2px 10px rgba(0,0,0,.25); animation:mi-nudge 1.4s ease-in-out infinite; }
.mi-pointer.mi-at-right{ right:calc(var(--map-info-side, 12px) + 50px); }
.mi-pointer.mi-at-left{ left:calc(var(--map-info-side, 12px) + 50px); flex-direction:row-reverse;
animation-name:mi-nudge-left; }
.mi-pointer svg{ width:16px; height:16px; flex:none; }
@keyframes mi-nudge{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(6px) } }
@keyframes mi-nudge-left{ 0%,100%{ transform:translateX(0) } 50%{ transform:translateX(-6px) } }
.mi-scrim{ position:fixed; inset:0; z-index:1000; background:rgba(15,20,35,.55);
display:flex; align-items:center; justify-content:center; padding:24px; }
.mi-dialog{ background:#fff; color:#1b1f2a; width:min(720px, 100%); max-height:min(88vh, 100%);
border-radius:14px; box-shadow:0 20px 60px rgba(0,0,0,.35); display:flex; flex-direction:column;
overflow:hidden; font-size:15px; line-height:1.5; }
.mi-head{ display:flex; align-items:flex-start; gap:12px; padding:18px 20px 12px; border-bottom:1px solid #e6e8ee; }
.mi-title{ margin:0; font-size:19px; line-height:1.3; flex:1; }
.mi-close{ flex:none; width:40px; height:40px; border-radius:50%; border:none; background:#eef1f6;
color:#1b1f2a; cursor:pointer; font-size:22px; line-height:1; }
.mi-close:focus-visible, .mi-toggle:focus-visible, .mi-hit:focus-visible, .mi-full:focus-visible{
outline:3px solid #2563eb; outline-offset:2px; }
.mi-body{ overflow-y:auto; padding:16px 20px 20px; -webkit-overflow-scrolling:touch; }
.mi-lead{ margin:0 0 6px; font-size:16px; }
.mi-takeaway{ margin:0 0 14px; color:#3b4256; }
.mi-takeaway b{ color:#1b1f2a; }
.mi-ex{ margin:18px 0 0; }
.mi-ex h3{ margin:0 0 8px; font-size:15px; text-transform:uppercase; letter-spacing:.04em; color:#556; }
.mi-img{ display:block; width:100%; height:auto; border-radius:8px; background:#f4f5f8;
box-shadow:0 1px 4px rgba(0,0,0,.15); }
.mi-poster{ max-width:480px; margin:0 auto; }
.mi-full{ display:inline-block; margin-top:6px; font-size:14px; color:#1d4ed8; }
.mi-hit{ display:block; width:100%; padding:0; border:none; background:none; cursor:zoom-in; text-align:inherit; }
.mi-hit.mi-open{ cursor:zoom-out; }
.mi-page{ max-width:420px; margin:0 auto; }
.mi-pages .mi-page + .mi-page{ margin-top:12px; }
.mi-stack{ position:relative; }
.mi-stack::after{ content:''; position:absolute; inset:6px -6px -6px 6px; z-index:-1; border-radius:8px;
background:#e3e6ee; box-shadow:0 1px 4px rgba(0,0,0,.15); }
.mi-toggle{ display:inline-flex; align-items:center; min-height:44px; margin-top:10px; padding:0 16px;
border-radius:999px; border:1px solid #c9cedb; background:#fff; color:#1b1f2a; font:inherit;
font-size:14px; font-weight:600; cursor:pointer; }
.mi-toggle:hover{ background:#f3f5fa; }
.mi-center{ text-align:center; }
.mi-deck{ position:relative; height:250px; max-width:340px; margin:0 auto; }
.mi-deck .mi-card{ position:absolute; top:10px; width:160px; height:224px; object-fit:cover;
border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,.25); background:#f4f5f8; }
.mi-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(120px, 1fr)); gap:10px; }
.mi-grid .mi-img{ aspect-ratio:5 / 7; object-fit:cover; }
.mi-table-wrap{ overflow-x:auto; border:1px solid #e1e4ec; border-radius:8px; }
.mi-table{ border-collapse:collapse; width:100%; font-size:13px; }
.mi-table th, .mi-table td{ padding:6px 10px; border-bottom:1px solid #eceef3; text-align:start; white-space:nowrap; }
.mi-table th{ background:#f5f6f9; font-weight:600; }
.mi-more{ margin:6px 0 0; font-size:13px; color:#556; }
.mi-note{ margin:18px 0 0; padding:10px 12px; border-radius:8px; background:#fff7e6; color:#5a4300; font-size:14px; }
.mi-missing{ padding:24px; border-radius:8px; background:#f4f5f8; color:#556; text-align:center; font-size:14px; }
.mi-foot{ padding:12px 20px; border-top:1px solid #e6e8ee; font-size:14px; }
.mi-foot label{ display:inline-flex; align-items:center; gap:8px; min-height:44px; cursor:pointer; }
.mi-foot input{ width:20px; height:20px; }
/* A phone gets the whole screen: a floating card with margins leaves too little room for a page. */
@media (max-width:600px){
.mi-scrim{ padding:0; align-items:stretch; }
.mi-dialog{ width:100%; max-height:none; height:100%; border-radius:0; }
.mi-pointer{ font-size:12px; padding:7px 10px; }
.mi-deck{ height:220px; }
.mi-deck .mi-card{ width:136px; height:190px; }
}
/* This component animates (the pointer's nudge), so it carries its own reduced-motion rule where its
styles are injected: a page-level rule cannot reach into a shadow root. */
@media (prefers-reduced-motion: reduce){
.mi-pointer{ animation:none; }
}
`, 'map-info')
</script>
<template>
<div class="mi" :dir="isRtl() ? 'rtl' : 'ltr'" :lang="lang()">
<button ref="btnEl" type="button" class="mi-btn" :class="placement === 'top-left' ? 'mi-at-left' : 'mi-at-right'"
:aria-label="t('mapInfo.open')" :title="t('mapInfo.open')" aria-haspopup="dialog" :aria-expanded="String(open)"
@click="show">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
<rect x="11" y="10" width="2" height="7" rx="1" fill="currentColor" />
<circle cx="12" cy="7" r="1.3" fill="currentColor" />
</svg>
</button>
<div v-if="pointing && !open" class="mi-pointer" :class="placement === 'top-left' ? 'mi-at-left' : 'mi-at-right'" aria-hidden="true">
<span>{{ t('mapInfo.pointer') }}</span>
<svg viewBox="0 0 16 16" focusable="false">
<path v-if="placement === 'top-left'" d="M14 8H3M7 3.5 2.5 8 7 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path v-else d="M2 8h11M9 3.5 13.5 8 9 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<div v-if="open" class="mi-scrim" @click.self="hide" @keydown="onKey">
<div class="mi-dialog" role="dialog" aria-modal="true" :aria-labelledby="'mi-title-' + storageKey">
<div class="mi-head">
<h2 :id="'mi-title-' + storageKey" class="mi-title">{{ t('mapInfo.title') }}</h2>
<button ref="closeEl" type="button" class="mi-close" :aria-label="t('mapInfo.close')" @click="hide">×</button>
</div>
<div class="mi-body">
<p v-if="leadKey || lead" class="mi-lead">{{ text(leadKey, lead) }}</p>
<p v-if="takeawayKey || takeaway" class="mi-takeaway"><b>{{ t('mapInfo.takeaway') }}</b> {{ text(takeawayKey, takeaway) }}</p>
<section v-for="(ex, i) in examples" :key="i" class="mi-ex">
<h3>{{ titleOf(ex) }}</h3>
<template v-if="ex.kind === 'image'">
<div v-if="failed[i]" class="mi-missing">{{ t('mapInfo.unavailable') }}</div>
<template v-else>
<img class="mi-img mi-poster" :src="url(ex.src)" :alt="ex.alt || titleOf(ex)" decoding="async" @error="failed[i] = true" />
<div v-if="ex.full" class="mi-center">
<a class="mi-full" :href="url(ex.full)" target="_blank" rel="noopener">{{ t('mapInfo.openFull') }}</a>
</div>
</template>
</template>
<template v-else-if="ex.kind === 'pages' && (ex.pages || []).length">
<div v-if="failed[i]" class="mi-missing">{{ t('mapInfo.unavailable') }}</div>
<template v-else-if="!expanded[i]">
<button type="button" class="mi-hit mi-page" :class="{ 'mi-stack': ex.pages.length > 1 }"
:aria-label="t('mapInfo.expandPages', { n: num(ex.pages.length) })" aria-expanded="false" @click="toggle(i)">
<img class="mi-img" :src="url(ex.pages[0])" :alt="titleOf(ex)" decoding="async" @error="failed[i] = true" />
</button>
<div v-if="ex.pages.length > 1" class="mi-center">
<button type="button" class="mi-toggle" aria-expanded="false" @click="toggle(i)">{{ t('mapInfo.expandPages', { n: num(ex.pages.length) }) }}</button>
</div>
</template>
<template v-else>
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapsePages') }}</button></div>
<div class="mi-pages">
<button v-for="(p, k) in ex.pages" :key="k" type="button" class="mi-hit mi-open mi-page"
:aria-label="t('mapInfo.collapsePages')" @click="toggle(i)">
<img class="mi-img" :src="url(p)" :alt="titleOf(ex) + ' ' + num(k + 1)" loading="lazy" decoding="async" />
</button>
</div>
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapsePages') }}</button></div>
</template>
</template>
<template v-else-if="ex.kind === 'deck' && (ex.cards || []).length">
<template v-if="!expanded[i]">
<button type="button" class="mi-hit mi-deck" :aria-label="t('mapInfo.expandDeck', { n: num(ex.cards.length) })"
aria-expanded="false" @click="toggle(i)">
<img v-for="(c, k) in deckTop(ex)" :key="k" class="mi-card" :src="url(c)" alt="" decoding="async"
:style="{ insetInlineStart: (k * 36) + 'px', transform: 'rotate(' + ((k - 2) * 4) + 'deg)', zIndex: 10 - k }" />
</button>
<div class="mi-center">
<button type="button" class="mi-toggle" aria-expanded="false" @click="toggle(i)">{{ t('mapInfo.expandDeck', { n: num(ex.cards.length) }) }}</button>
</div>
</template>
<template v-else>
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapseDeck') }}</button></div>
<div class="mi-grid">
<img v-for="(c, k) in ex.cards" :key="k" class="mi-img" :src="url(c)" :alt="titleOf(ex) + ' ' + num(k + 1)" loading="lazy" decoding="async" />
</div>
<div class="mi-center"><button type="button" class="mi-toggle" aria-expanded="true" @click="toggle(i)">{{ t('mapInfo.collapseDeck') }}</button></div>
</template>
</template>
<template v-else-if="ex.kind === 'table' && (ex.columns || []).length">
<div class="mi-table-wrap">
<table class="mi-table">
<thead><tr><th v-for="(c, k) in ex.columns" :key="k" scope="col">{{ colLabel(c) }}</th></tr></thead>
<tbody><tr v-for="(r, k) in ex.rows || []" :key="k"><td v-for="(v, j) in r" :key="j">{{ typeof v === 'number' ? num(v) : v }}</td></tr></tbody>
</table>
</div>
<p v-if="ex.total && ex.total > (ex.rows || []).length" class="mi-more">
{{ t('mapInfo.moreRows', { n: num(ex.total - (ex.rows || []).length) }) }}
</p>
</template>
</section>
<p v-if="noteKey || note" class="mi-note">{{ text(noteKey, note) }}</p>
</div>
<div class="mi-foot">
<label>
<input type="checkbox" :checked="pointerOff" @change="setPointerOff($event.target.checked)" />
<span>{{ t('mapInfo.pointerOff') }}</span>
</label>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,20 @@
{
"open": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
"title": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
"pointer": "ماذا تفعل هذه الخريطة؟",
"close": "إغلاق",
"takeaway": "ما تحصل عليه:",
"poster": "ملصق",
"packet": "حزمة الصلاة",
"cards": "بطاقات الصلاة",
"spreadsheet": "جدول بيانات",
"example": "مثال",
"expandPages": "عرض كل الصفحات ({n})",
"collapsePages": "عرض الصفحة الأولى فقط",
"expandDeck": "عرض كل البطاقات ({n})",
"collapseDeck": "تجميع البطاقات",
"moreRows": "صفوف إضافية في جدول البيانات الكامل: {n}",
"openFull": "فتح بالحجم الكامل",
"unavailable": "تعذّر تحميل هذا المثال.",
"pointerOff": "لا تُشِر إلى هذا الزر مرة أخرى"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "لم يُحدَّد شيء",
"selectionCardsFailed": "فشل إنشاء بطاقات التحديد: {reason}",
"selectionPosterNeedsZone": "الملصقات متاحة لكل منطقة في مفتاح الخريطة — استخدم «قائمة الصلاة» لتحديد على الخريطة",
"mapDataFailed": "تعذّر تحميل بيانات الخريطة"
"mapDataFailed": "تعذّر تحميل بيانات الخريطة",
"infoLead": "قائمة UPG-100 على خريطة واحدة: الشعوب غير المُبشَّرة، مجمّعة في مناطق يمكنك أن تتبنّاها بالصلاة.",
"infoTakeaway": "لكل منطقة: ملصق للحائط، وحزمة صلاة للتوزيع، ومجموعة من بطاقات الصلاة. المثال أدناه هو المنطقة التي تضم أكبر عدد من الناس في شعوب لا يعمل أحد بعد على غرس كنائس بينها."
}

View file

@ -0,0 +1,20 @@
{
"open": "Was kann diese Karte für Sie tun?",
"title": "Was kann diese Karte für Sie tun?",
"pointer": "Was kann diese Karte?",
"close": "Schließen",
"takeaway": "Was Sie mitnehmen:",
"poster": "Poster",
"packet": "Gebetsheft",
"cards": "Gebetskarten",
"spreadsheet": "Tabelle",
"example": "Beispiel",
"expandPages": "Alle Seiten anzeigen ({n})",
"collapsePages": "Nur die erste Seite anzeigen",
"expandDeck": "Alle Karten anzeigen ({n})",
"collapseDeck": "Karten stapeln",
"moreRows": "Weitere Zeilen in der vollständigen Tabelle: {n}",
"openFull": "In voller Größe öffnen",
"unavailable": "Dieses Beispiel konnte nicht geladen werden.",
"pointerOff": "Nicht mehr auf diese Schaltfläche hinweisen"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Nichts ausgewählt",
"selectionCardsFailed": "Karten der Auswahl fehlgeschlagen: {reason}",
"selectionPosterNeedsZone": "Plakate gibt es pro Gebiet in der Legende – nutzen Sie Gebetsliste für eine Auswahl auf der Karte",
"mapDataFailed": "Die Kartendaten konnten nicht geladen werden"
"mapDataFailed": "Die Kartendaten konnten nicht geladen werden",
"infoLead": "Die UPG-100-Liste auf einer Karte: unerreichte Volksgruppen, zusammengefasst in Zonen, die Sie im Gebet übernehmen können.",
"infoTakeaway": "für jede Zone ein Wandposter, ein Gebetsheft zum Weitergeben und ein Satz Gebetskarten. Das Beispiel unten ist die Zone mit den meisten Menschen in Volksgruppen, unter denen noch niemand Gemeinden gründet."
}

View file

@ -0,0 +1,20 @@
{
"open": "What can this map do for you?",
"title": "What can this map do for you?",
"pointer": "What can this map do?",
"close": "Close",
"takeaway": "What you take away:",
"poster": "Poster",
"packet": "Prayer packet",
"cards": "Prayer cards",
"spreadsheet": "Spreadsheet",
"example": "Example",
"expandPages": "Show all pages ({n})",
"collapsePages": "Show the first page only",
"expandDeck": "Show all cards ({n})",
"collapseDeck": "Stack the cards",
"moreRows": "More rows in the full spreadsheet: {n}",
"openFull": "Open full size",
"unavailable": "This example could not be loaded.",
"pointerOff": "Don't point at this button again"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Nothing is selected",
"selectionCardsFailed": "Selection card deck failed: {reason}",
"selectionPosterNeedsZone": "Posters exist per territory on the legend — use Prayer List for a map selection",
"mapDataFailed": "The map data could not be loaded"
"mapDataFailed": "The map data could not be loaded",
"infoLead": "The UPG-100 list on one map: unreached people groups, gathered into zones you can adopt in prayer.",
"infoTakeaway": "for any zone, a wall poster, a prayer packet to hand out and a deck of prayer cards. The example below is the zone with the most people in groups where no one is planting churches yet."
}

View file

@ -0,0 +1,20 @@
{
"open": "¿Qué puede hacer este mapa por usted?",
"title": "¿Qué puede hacer este mapa por usted?",
"pointer": "¿Qué hace este mapa?",
"close": "Cerrar",
"takeaway": "Lo que se lleva:",
"poster": "Póster",
"packet": "Paquete de oración",
"cards": "Tarjetas de oración",
"spreadsheet": "Hoja de cálculo",
"example": "Ejemplo",
"expandPages": "Ver todas las páginas ({n})",
"collapsePages": "Ver solo la primera página",
"expandDeck": "Ver todas las tarjetas ({n})",
"collapseDeck": "Apilar las tarjetas",
"moreRows": "Más filas en la hoja de cálculo completa: {n}",
"openFull": "Abrir a tamaño completo",
"unavailable": "No se pudo cargar este ejemplo.",
"pointerOff": "No volver a señalar este botón"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "No hay nada seleccionado",
"selectionCardsFailed": "Error al generar las tarjetas de la selección: {reason}",
"selectionPosterNeedsZone": "Hay carteles por territorio en la leyenda; usa Lista de oración para una selección en el mapa",
"mapDataFailed": "No se pudieron cargar los datos del mapa"
"mapDataFailed": "No se pudieron cargar los datos del mapa",
"infoLead": "La lista UPG-100 en un solo mapa: pueblos no alcanzados, agrupados en zonas que usted puede adoptar en oración.",
"infoTakeaway": "para cada zona, un póster para la pared, un paquete de oración para repartir y un mazo de tarjetas de oración. El ejemplo de abajo es la zona con más personas en pueblos donde todavía nadie está plantando iglesias."
}

View file

@ -0,0 +1,20 @@
{
"open": "Que peut faire cette carte pour vous ?",
"title": "Que peut faire cette carte pour vous ?",
"pointer": "Que fait cette carte ?",
"close": "Fermer",
"takeaway": "Ce que vous emportez :",
"poster": "Affiche",
"packet": "Livret de prière",
"cards": "Cartes de prière",
"spreadsheet": "Tableur",
"example": "Exemple",
"expandPages": "Afficher toutes les pages ({n})",
"collapsePages": "N'afficher que la première page",
"expandDeck": "Afficher toutes les cartes ({n})",
"collapseDeck": "Empiler les cartes",
"moreRows": "Lignes supplémentaires dans le tableur complet : {n}",
"openFull": "Ouvrir en taille réelle",
"unavailable": "Impossible de charger cet exemple.",
"pointerOff": "Ne plus signaler ce bouton"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Rien n’est sélectionné",
"selectionCardsFailed": "Échec des cartes de la sélection : {reason}",
"selectionPosterNeedsZone": "Les affiches existent par territoire dans la légende — utilisez Liste de prière pour une sélection sur la carte",
"mapDataFailed": "Les données de la carte n’ont pas pu être chargées"
"mapDataFailed": "Les données de la carte n’ont pas pu être chargées",
"infoLead": "La liste UPG-100 sur une seule carte : les peuples non atteints, regroupés en zones que vous pouvez adopter dans la prière.",
"infoTakeaway": "pour chaque zone, une affiche murale, un livret de prière à distribuer et un jeu de cartes de prière. L'exemple ci-dessous est la zone qui compte le plus de personnes dans des peuples où personne n'implante encore d'Églises."
}

View file

@ -0,0 +1,20 @@
{
"open": "यह नक्शा आपके लिए क्या कर सकता है?",
"title": "यह नक्शा आपके लिए क्या कर सकता है?",
"pointer": "यह नक्शा क्या करता है?",
"close": "बंद करें",
"takeaway": "आपको क्या मिलता है:",
"poster": "पोस्टर",
"packet": "प्रार्थना पुस्तिका",
"cards": "प्रार्थना कार्ड",
"spreadsheet": "स्प्रेडशीट",
"example": "उदाहरण",
"expandPages": "सभी पृष्ठ दिखाएँ ({n})",
"collapsePages": "केवल पहला पृष्ठ दिखाएँ",
"expandDeck": "सभी कार्ड दिखाएँ ({n})",
"collapseDeck": "कार्ड एक साथ रखें",
"moreRows": "पूरी स्प्रेडशीट में और पंक्तियाँ: {n}",
"openFull": "पूरे आकार में खोलें",
"unavailable": "यह उदाहरण लोड नहीं हो सका।",
"pointerOff": "इस बटन की ओर फिर से इशारा न करें"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "कुछ भी चुना नहीं गया है",
"selectionCardsFailed": "चयन के कार्ड विफल: {reason}",
"selectionPosterNeedsZone": "पोस्टर लेजेंड में हर क्षेत्र के लिए हैं — मानचित्र पर चयन के लिए प्रार्थना सूची का उपयोग करें",
"mapDataFailed": "मानचित्र का डेटा लोड नहीं हो सका"
"mapDataFailed": "मानचित्र का डेटा लोड नहीं हो सका",
"infoLead": "UPG-100 सूची एक ही नक्शे पर: अपहुँचे जन-समूह, क्षेत्रों में बाँटे गए, जिन्हें आप प्रार्थना में अपना सकते हैं।",
"infoTakeaway": "हर क्षेत्र के लिए दीवार का एक पोस्टर, बाँटने के लिए एक प्रार्थना पुस्तिका और प्रार्थना कार्डों का एक सेट। नीचे का उदाहरण वह क्षेत्र है जहाँ ऐसे जन-समूहों में सबसे अधिक लोग हैं जिनके बीच अभी कोई कलीसिया स्थापित नहीं कर रहा है।"
}

View file

@ -0,0 +1,20 @@
{
"open": "Che cosa può fare questa mappa per te?",
"title": "Che cosa può fare questa mappa per te?",
"pointer": "Che cosa fa questa mappa?",
"close": "Chiudi",
"takeaway": "Che cosa ottieni:",
"poster": "Poster",
"packet": "Libretto di preghiera",
"cards": "Carte di preghiera",
"spreadsheet": "Foglio di calcolo",
"example": "Esempio",
"expandPages": "Mostra tutte le pagine ({n})",
"collapsePages": "Mostra solo la prima pagina",
"expandDeck": "Mostra tutte le carte ({n})",
"collapseDeck": "Impila le carte",
"moreRows": "Altre righe nel foglio di calcolo completo: {n}",
"openFull": "Apri a grandezza intera",
"unavailable": "Impossibile caricare questo esempio.",
"pointerOff": "Non indicare più questo pulsante"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Nessuna selezione",
"selectionCardsFailed": "Carte della selezione non riuscite: {reason}",
"selectionPosterNeedsZone": "I manifesti esistono per territorio nella legenda: usa Lista di preghiera per una selezione sulla mappa",
"mapDataFailed": "Impossibile caricare i dati della mappa"
"mapDataFailed": "Impossibile caricare i dati della mappa",
"infoLead": "La lista UPG-100 su un'unica mappa: popoli non raggiunti, raggruppati in zone che puoi adottare nella preghiera.",
"infoTakeaway": "per ogni zona, un poster da parete, un libretto di preghiera da distribuire e un mazzo di carte di preghiera. L'esempio qui sotto è la zona con più persone in popoli dove nessuno sta ancora fondando chiese."
}

View file

@ -0,0 +1,20 @@
{
"open": "O que este mapa pode fazer por você?",
"title": "O que este mapa pode fazer por você?",
"pointer": "O que este mapa faz?",
"close": "Fechar",
"takeaway": "O que você leva:",
"poster": "Pôster",
"packet": "Pacote de oração",
"cards": "Cartões de oração",
"spreadsheet": "Planilha",
"example": "Exemplo",
"expandPages": "Mostrar todas as páginas ({n})",
"collapsePages": "Mostrar só a primeira página",
"expandDeck": "Mostrar todos os cartões ({n})",
"collapseDeck": "Empilhar os cartões",
"moreRows": "Mais linhas na planilha completa: {n}",
"openFull": "Abrir em tamanho real",
"unavailable": "Não foi possível carregar este exemplo.",
"pointerOff": "Não apontar mais para este botão"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Nada está selecionado",
"selectionCardsFailed": "Falha ao gerar os cartões da seleção: {reason}",
"selectionPosterNeedsZone": "Há cartazes por território na legenda — use Lista de oração para uma seleção no mapa",
"mapDataFailed": "Não foi possível carregar os dados do mapa"
"mapDataFailed": "Não foi possível carregar os dados do mapa",
"infoLead": "A lista UPG-100 em um só mapa: povos não alcançados, agrupados em zonas que você pode adotar em oração.",
"infoTakeaway": "para cada zona, um pôster de parede, um pacote de oração para distribuir e um baralho de cartões de oração. O exemplo abaixo é a zona com mais pessoas em povos onde ninguém ainda está plantando igrejas."
}

View file

@ -0,0 +1,20 @@
{
"open": "Ce poate face această hartă pentru tine?",
"title": "Ce poate face această hartă pentru tine?",
"pointer": "Ce face această hartă?",
"close": "Închide",
"takeaway": "Ce primești:",
"poster": "Poster",
"packet": "Pachet de rugăciune",
"cards": "Cartonașe de rugăciune",
"spreadsheet": "Foaie de calcul",
"example": "Exemplu",
"expandPages": "Arată toate paginile ({n})",
"collapsePages": "Arată doar prima pagină",
"expandDeck": "Arată toate cartonașele ({n})",
"collapseDeck": "Strânge cartonașele",
"moreRows": "Rânduri suplimentare în foaia de calcul completă: {n}",
"openFull": "Deschide la dimensiune completă",
"unavailable": "Acest exemplu nu a putut fi încărcat.",
"pointerOff": "Nu mai indica acest buton"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Nimic nu este selectat",
"selectionCardsFailed": "Cărțile selecției au eșuat: {reason}",
"selectionPosterNeedsZone": "Afișele există pe teritoriu în legendă — folosiți Listă de rugăciune pentru o selecție pe hartă",
"mapDataFailed": "Datele hărții nu au putut fi încărcate"
"mapDataFailed": "Datele hărții nu au putut fi încărcate",
"infoLead": "Lista UPG-100 pe o singură hartă: popoare neatinse, grupate în zone pe care le poți adopta în rugăciune.",
"infoTakeaway": "pentru fiecare zonă, un poster de perete, un pachet de rugăciune de împărțit și un set de cartonașe de rugăciune. Exemplul de mai jos este zona cu cei mai mulți oameni din popoare în care nimeni nu plantează încă biserici."
}

View file

@ -0,0 +1,20 @@
{
"open": "Чем эта карта может быть вам полезна?",
"title": "Чем эта карта может быть вам полезна?",
"pointer": "Что умеет эта карта?",
"close": "Закрыть",
"takeaway": "Что вы получаете:",
"poster": "Плакат",
"packet": "Молитвенный буклет",
"cards": "Молитвенные карточки",
"spreadsheet": "Таблица",
"example": "Пример",
"expandPages": "Показать все страницы ({n})",
"collapsePages": "Показать только первую страницу",
"expandDeck": "Показать все карточки ({n})",
"collapseDeck": "Сложить карточки в стопку",
"moreRows": "Ещё строк в полной таблице: {n}",
"openFull": "Открыть в полном размере",
"unavailable": "Не удалось загрузить этот пример.",
"pointerOff": "Больше не указывать на эту кнопку"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "Ничего не выбрано",
"selectionCardsFailed": "Не удалось создать карточки выбора: {reason}",
"selectionPosterNeedsZone": "Плакаты есть для каждой территории в легенде — для выбора на карте используйте «Молитвенный список»",
"mapDataFailed": "Не удалось загрузить данные карты"
"mapDataFailed": "Не удалось загрузить данные карты",
"infoLead": "Список UPG-100 на одной карте: недостигнутые народы, объединённые в зоны, которые вы можете взять на молитвенное попечение.",
"infoTakeaway": "для каждой зоны — настенный плакат, молитвенный буклет для раздачи и набор молитвенных карточек. Ниже показана зона, где больше всего людей в народах, среди которых пока никто не основывает церкви."
}

View file

@ -0,0 +1,20 @@
{
"open": "这张地图能为你做什么?",
"title": "这张地图能为你做什么?",
"pointer": "这张地图能做什么?",
"close": "关闭",
"takeaway": "你能带走的:",
"poster": "海报",
"packet": "祷告手册",
"cards": "祷告卡",
"spreadsheet": "电子表格",
"example": "示例",
"expandPages": "显示全部页面({n})",
"collapsePages": "只显示第一页",
"expandDeck": "显示全部卡片({n})",
"collapseDeck": "把卡片叠起来",
"moreRows": "完整电子表格中的更多行:{n}",
"openFull": "以完整尺寸打开",
"unavailable": "无法加载此示例。",
"pointerOff": "不再指向这个按钮"
}

View file

@ -3,5 +3,7 @@
"nothingSelected": "未选择任何内容",
"selectionCardsFailed": "所选内容的卡片生成失败:{reason}",
"selectionPosterNeedsZone": "海报按图例中的区域提供——地图上的选择请使用“祷告名单”",
"mapDataFailed": "无法加载地图数据"
"mapDataFailed": "无法加载地图数据",
"infoLead": "UPG-100 名单尽在一张地图上:未得之民按区域分组,你可以在祷告中认领一个区域。",
"infoTakeaway": "每个区域都有一张墙面海报、一本可分发的祷告手册和一套祷告卡。下面的示例是这样一个区域:在尚无人植堂的族群中,这里的人数最多。"
}