Changeset c26cc18 in Klonkt
- Timestamp:
- 07/24/2026 06:04:03 PM (7 weeks ago)
- Branches:
- main
- Children:
- 780a7c6
- Parents:
- 1a2d8ac
- Location:
- src
- Files:
-
- 7 edited
-
assets/css/guardian.css (modified) (2 diffs)
-
assets/js/guardian.js (modified) (6 diffs)
-
routes/guardian.js (modified) (3 diffs)
-
services/ActivityPubService.js (modified) (1 diff)
-
services/guardianship/handshake.js (modified) (2 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/guardian.ejs (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/guardian.css
r1a2d8ac rc26cc18 6 6 --ink: #e8ecf2; 7 7 --sub: #93a0b4; 8 --line: #2c3646; 8 9 --accent: #ff6b35; 9 10 --ok: #4caf7d; … … 15 16 color: var(--ink); 16 17 font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; 17 padding-bottom: 4 0px;18 padding-bottom: 48px; 18 19 } 19 20 .g-head { 20 21 display: flex; 21 22 align-items: center; 22 gap: 1 0px;23 padding: 1 4px 16px;23 gap: 12px; 24 padding: 16px; 24 25 background: var(--card); 26 border-bottom: 1px solid var(--line); 25 27 position: sticky; 26 28 top: 0; 29 z-index: 2; 27 30 } 28 .g-head h1 { font-size: 1.15rem; margin: 0; flex: 1; } 29 .g-buoy { font-size: 1.5rem; } 30 .g-me { color: var(--sub); font-size: .9rem; } 31 .g-head-txt { flex: 1; } 32 .g-head h1 { font-size: 1.15rem; margin: 0; } 33 .g-head .g-sub { margin: 0; color: var(--sub); font-size: .78rem; } 34 .g-buoy { font-size: 1.7rem; } 35 .g-me { color: var(--sub); font-size: .85rem; } 31 36 #site-picker { 32 37 background: var(--bg); color: var(--ink); 33 border: 1px solid #33405233; border-radius: 8px; padding: 6px 8px;38 border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; 34 39 } 35 main { max-width: 640px; margin: 0 auto; padding: 0 16px; } 36 h2 { font-size: 1rem; margin: 26px 0 10px; } 37 .g-sub { color: var(--sub); font-size: .85rem; margin: 0 0 10px; } 38 .g-empty { color: var(--sub); font-size: .9rem; } 39 .g-msg { font-size: .85rem; color: var(--ok); } 40 main { max-width: 640px; margin: 0 auto; padding: 8px 16px; } 41 42 section { padding: 18px 0; border-bottom: 1px solid var(--line); } 43 section:last-child { border-bottom: 0; } 44 .g-sec-head { display: flex; align-items: center; gap: 8px; } 45 .g-sec-head h2 { font-size: 1.02rem; margin: 0; } 46 .g-badge { 47 background: var(--accent); color: #fff; font-size: .72rem; font-weight: 700; 48 min-width: 20px; text-align: center; border-radius: 10px; padding: 1px 7px; 49 } 50 .g-sec-sub { color: var(--sub); font-size: .84rem; margin: 4px 0 12px; } 51 .g-empty { color: var(--sub); font-size: .9rem; margin: 6px 0 0; } 52 .g-msg { font-size: .85rem; color: var(--ok); margin: 8px 0 0; } 40 53 .g-msg.err { color: #ff7a7a; } 41 54 .g-list { display: flex; flex-direction: column; gap: 8px; } 42 55 43 .g-card { 44 background: var(--card); 45 border-radius: 12px; 46 padding: 12px 14px; 47 } 56 .g-card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; } 48 57 .g-card.help { border-left: 3px solid var(--accent); } 49 .g-card .who { font-weight: 600; }50 .g-card .when { color: var(--sub); font-size: .78rem; }51 .g-card .body { margin-top: 6px; font-size: .92rem; overflow-wrap: anywhere; }52 .g-card .body img { max-width: 100%; border-radius: 8px; }53 58 .g-card .row { display: flex; align-items: center; gap: 10px; } 54 .g-card .row .grow { flex: 1; } 55 .g-card .pend { color: var(--sub); font-size: .8rem; } 59 .g-card .row .grow { flex: 1; min-width: 0; } 60 .g-card .who { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 61 .g-card .when { color: var(--sub); font-size: .76rem; white-space: nowrap; } 62 .g-card .body { margin-top: 8px; font-size: .92rem; overflow-wrap: anywhere; } 63 .g-card .body img { max-width: 100%; border-radius: 8px; margin-top: 6px; } 64 .g-link { display: inline-block; margin-top: 8px; color: var(--accent); font-size: .82rem; text-decoration: none; } 56 65 57 #adopt-form { display: flex; gap: 8px; margin-bottom: 8px; } 66 .tag { font-size: .72rem; font-weight: 600; padding: 3px 9px; border-radius: 20px; white-space: nowrap; } 67 .tag.wait { background: #3a2f1a; color: #e8b04b; } 68 .tag.ok { background: #17301f; color: var(--ok); } 69 70 #adopt-form { display: flex; gap: 8px; } 58 71 #adopt-form input { 59 flex: 1; 60 background: var(-- card); color: var(--ink);61 border: 1px solid #33405255; border-radius: 10px; padding: 10px 12px;72 flex: 1; min-width: 0; 73 background: var(--bg); color: var(--ink); 74 border: 1px solid var(--line); border-radius: 10px; padding: 11px 12px; 62 75 } 63 76 button { 64 77 background: var(--accent); color: #fff; border: 0; 65 border-radius: 10px; padding: 1 0px 16px; font-weight: 600; cursor: pointer;78 border-radius: 10px; padding: 11px 18px; font-weight: 600; cursor: pointer; 66 79 } 67 button.quiet { background: #33405255; color: var(--ink); font-weight: 500; } 80 button:disabled { opacity: .5; cursor: default; } 81 button.quiet { background: transparent; color: var(--sub); border: 1px solid var(--line); font-weight: 500; } 82 button.quiet.is-on { color: var(--ok); border-color: var(--ok); } 83 button.small { padding: 6px 12px; font-size: .82rem; } -
src/assets/js/guardian.js
r1a2d8ac rc26cc18 1 /* Guardian PWA client (FEP-633c): renders the dashboard state, adopts wards,2 and manages the guardian push channel (web-push slice reused: alert types3 'help' + 'guardian'). No framework, no inline scripts (CSP). */1 /* Guardian PWA client (FEP-633c): renders the dashboard, adopts wards, and 2 manages the guardian push channel. No framework, no inline scripts (CSP). 3 All user-facing text comes from state.strings (server i18n). */ 4 4 (function () { 5 5 'use strict'; 6 var state = JSON.parse(document.getElementById('guardian-state').textContent || '{}'); 6 var S = JSON.parse(document.getElementById('guardian-state').textContent || '{}'); 7 var T = S.strings || {}; 7 8 8 9 function el(tag, cls, text) { … … 17 18 catch (e) { return uri; } 18 19 } 19 function when(s) { 20 return String(s || '').slice(0, 16).replace('T', ' '); 21 } 20 function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); } 21 function show(id, on) { document.getElementById(id).hidden = !on; } 22 22 23 // ── Message centre: help requests──────────────────────────────────────23 // ── 1. Help requests ─────────────────────────────────────────────────── 24 24 function renderHelp() { 25 25 var list = document.getElementById('help-list'); 26 26 list.textContent = ''; 27 (state.help || []).forEach(function (h) { 27 var help = S.help || []; 28 help.forEach(function (h) { 28 29 var card = el('div', 'g-card help'); 29 30 var row = el('div', 'row'); 30 var who = el('span', 'who', h.actor_name || handleOf(h.actor_uri, h.actor_handle)); 31 var at = el('span', 'when', when(h.published || h.created_at)); 32 row.appendChild(who); row.appendChild(at); 31 row.appendChild(el('span', 'who grow', h.actor_name || handleOf(h.actor_uri, h.actor_handle))); 32 row.appendChild(el('span', 'when', when(h.published || h.created_at))); 33 33 card.appendChild(row); 34 34 var body = el('div', 'body'); 35 body.innerHTML = h.content || ''; // sanitized server-side on ingest35 body.innerHTML = h.content || ''; // sanitized server-side on ingest 36 36 card.appendChild(body); 37 37 if (h.note_url) { 38 var link = el('a', 'when','open');39 link.href = h.note_url; link.target = '_blank'; link.rel = 'noopener';40 card.appendChild( link);38 var a = el('a', 'g-link', T.open || 'open'); 39 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener'; 40 card.appendChild(a); 41 41 } 42 42 list.appendChild(card); 43 43 }); 44 document.getElementById('help-empty').hidden = (state.help || []).length > 0; 44 var badge = document.getElementById('help-count'); 45 badge.textContent = help.length; badge.hidden = help.length === 0; 46 show('help-empty', help.length === 0); 45 47 } 46 48 47 // ── Wards + pending offers ───────────────────────────────────────────── 48 function wardCard(w, pending) { 49 var card = el('div', 'g-card'); 50 var row = el('div', 'row'); 51 var who = el('span', 'who grow', handleOf(w.other_uri, w.other_handle)); 52 row.appendChild(who); 53 if (pending) row.appendChild(el('span', 'pend', state.strings.pending)); 54 var btn = el('button', 'quiet', pending ? state.strings.retract : state.strings.release); 55 btn.addEventListener('click', function () { 56 fetch('/guardian/wards/remove', { 57 method: 'POST', headers: { 'Content-Type': 'application/json' }, 58 body: JSON.stringify({ uri: w.other_uri, site: state.site }), 59 }).then(refresh); 49 // ── 3. Pending offers I sent ─────────────────────────────────────────── 50 function renderPending() { 51 var list = document.getElementById('pending-list'); 52 list.textContent = ''; 53 var pend = S.pendingOffers || []; 54 pend.forEach(function (w) { 55 var card = el('div', 'g-card'); 56 var row = el('div', 'row'); 57 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 58 row.appendChild(el('span', 'tag wait', T.pending)); 59 var btn = el('button', 'quiet small', T.retract); 60 btn.addEventListener('click', function () { remove(w.other_uri, btn); }); 61 row.appendChild(btn); 62 card.appendChild(row); 63 list.appendChild(card); 60 64 }); 61 row.appendChild(btn); 62 card.appendChild(row); 63 return card; 64 } 65 function renderWards() { 66 var wl = document.getElementById('wards-list'); 67 var ol = document.getElementById('offers-list'); 68 wl.textContent = ''; ol.textContent = ''; 69 (state.wards || []).forEach(function (w) { wl.appendChild(wardCard(w, false)); }); 70 (state.pendingOffers || []).forEach(function (w) { ol.appendChild(wardCard(w, true)); }); 71 document.getElementById('wards-empty').hidden = 72 (state.wards || []).length + (state.pendingOffers || []).length > 0; 65 show('pending-section', pend.length > 0); 73 66 } 74 67 75 function refresh() { 76 fetch('/guardian/api/state?site=' + encodeURIComponent(state.site)) 77 .then(function (r) { return r.json(); }) 78 .then(function (s) { if (s && !s.error) { state = s; renderHelp(); renderWards(); } }); 68 // ── 4. Accepted wards ────────────────────────────────────────────────── 69 function renderWards() { 70 var list = document.getElementById('wards-list'); 71 list.textContent = ''; 72 var wards = S.wards || []; 73 wards.forEach(function (w) { 74 var card = el('div', 'g-card'); 75 var row = el('div', 'row'); 76 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 77 row.appendChild(el('span', 'tag ok', T.active)); 78 var btn = el('button', 'quiet small', T.release); 79 btn.addEventListener('click', function () { remove(w.other_uri, btn); }); 80 row.appendChild(btn); 81 card.appendChild(row); 82 list.appendChild(card); 83 }); 84 show('wards-empty', wards.length === 0); 79 85 } 80 86 81 // ── Adopt ────────────────────────────────────────────────────────────── 82 document.getElementById('adopt-form').addEventListener('submit', function (ev) { 87 function remove(uri, btn) { 88 btn.disabled = true; 89 fetch('/guardian/wards/remove', { 90 method: 'POST', headers: { 'Content-Type': 'application/json' }, 91 body: JSON.stringify({ uri: uri, site: S.site }), 92 }).then(refresh); 93 } 94 95 function renderAll() { renderHelp(); renderPending(); renderWards(); } 96 97 function refresh() { 98 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site)) 99 .then(function (r) { return r.json(); }) 100 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } }); 101 } 102 103 // ── 2. Adopt ─────────────────────────────────────────────────────────── 104 var form = document.getElementById('adopt-form'); 105 var input = document.getElementById('adopt-handle'); 106 var adoptBtn = document.getElementById('adopt-btn'); 107 var msg = document.getElementById('adopt-msg'); 108 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; } 109 110 form.addEventListener('submit', function (ev) { 83 111 ev.preventDefault(); 84 var input = document.getElementById('adopt-handle');85 var msg = document.getElementById('adopt-msg');86 112 var handle = input.value.trim(); 87 113 if (!handle) return; 88 msg.hidden = false; msg.classList.remove('err'); msg.textContent = '…'; 114 adoptBtn.disabled = true; 115 setMsg(T.sending || '…', false); 89 116 fetch('/guardian/adopt', { 90 117 method: 'POST', headers: { 'Content-Type': 'application/json' }, 91 body: JSON.stringify({ handle: handle, site: state.site }),118 body: JSON.stringify({ handle: handle, site: S.site }), 92 119 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) 93 120 .then(function (res) { 94 if (res.ok) { msg.textContent = state.strings.sent; input.value = ''; refresh(); } 95 else { msg.classList.add('err'); msg.textContent = state.strings.failed + ': ' + (res.j.error || '?'); } 121 adoptBtn.disabled = false; 122 if (res.ok) { 123 input.value = ''; 124 // Always refresh: the offer is recorded even if delivery is still 125 // in flight. Show it under "Verzonden aanvragen". 126 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false); 127 refresh(); 128 } else { 129 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true); 130 } 96 131 }) 97 .catch(function () { msg.classList.add('err'); msg.textContent = state.strings.network; });132 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); }); 98 133 }); 99 134 … … 104 139 }); 105 140 106 // ── Push: the guardian channel (help + guardian alerts)────────────────141 // ── 5. Push ──────────────────────────────────────────────────────────── 107 142 var toggle = document.getElementById('push-toggle'); 108 143 var pmsg = document.getElementById('push-msg'); … … 115 150 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel; 116 151 toggle.dataset.subscribed = sub ? '1' : ''; 152 toggle.classList.toggle('is-on', !!sub); 117 153 }); 118 154 } … … 145 181 body: JSON.stringify({ 146 182 subscription: sub.toJSON(), 147 // The guardian channel: calls for help + adoption traffic.148 183 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 }, 149 184 uaLabel: 'Guardian PWA', … … 151 186 }); 152 187 }).then(pushState).catch(function (e) { 153 pmsg.hidden = false; pmsg.class List.add('err');154 pmsg.textContent = 'Push niet beschikbaar: ' + e.message;188 pmsg.hidden = false; pmsg.className = 'g-msg err'; 189 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message; 155 190 }); 156 191 }); 157 192 }); 158 193 159 renderHelp(); renderWards(); pushState(); 160 // Live-ish: poll the state every 45s while the PWA is open. 161 setInterval(refresh, 45000); 194 renderAll(); pushState(); 195 setInterval(refresh, 45000); // live-ish while open 162 196 })(); -
src/routes/guardian.js
r1a2d8ac rc26cc18 31 31 /** Everything the dashboard shows, one shape for page and API. */ 32 32 function uiStrings(L) { 33 const keys = ['sent', 'failed', 'network', 'pending', 'retract', 'release']; 33 const keys = ['sent', 'sent_retry', 'sending', 'not_found', 'failed', 'network', 34 'pending', 'active', 'retract', 'release', 'open', 'push_unavailable']; 34 35 return Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)])); 35 36 } … … 80 81 if (!handle) return res.status(400).json({ error: 'empty_handle' }); 81 82 const wardUri = /^https?:\/\//i.test(handle) ? handle : await AP.webfingerResolve(handle).catch(() => null); 82 if (!wardUri) return res.status(404).json({ error: 'not_found' }); 83 if (!wardUri) return res.status(404).json({ error: 'not_found' }); // the handle does not resolve to an account 83 84 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 84 85 const me = AP.actorId(base, site.slug); … … 87 88 object: { type: 'Relationship', subject: wardUri, relationship: 'shaer:Guardian', object: me }, 88 89 }); 89 if (!r || r.status >= 400) return res.status(r?.status || 500).json({ error: r?.error || 'offer_failed' }); 90 res.json({ ok: true, ward: wardUri }); 90 // 403/400 = a real refusal (e.g. you are a ward yourself); anything else the 91 // offer is recorded and delivery is retried in the background. 92 if (!r || (r.status >= 400 && r.status !== 502)) return res.status(r?.status || 500).json({ error: r?.error || 'offer_failed' }); 93 res.json({ ok: true, ward: wardUri, delivered: r.delivered !== false }); 91 94 }); 92 95 -
src/services/ActivityPubService.js
r1a2d8ac rc26cc18 3129 3129 return actorId(base, slug); 3130 3130 } 3131 // Deliver one activity to one actor's inbox, signed; queued on failure. 3131 // Deliver one activity to one actor's inbox, signed; queued + retried on any 3132 // hiccup so a slow or briefly-down ward server never loses the offer. Returns 3133 // { delivered, inbox }: delivered=false means the account could not be 3134 // resolved at all (a bad handle) — the offer stays recorded regardless. 3132 3135 async function deliverToActor(site, actorUri, activity) { 3133 const a = await fetchActor(actorUri).catch(() => null);3134 const inbox = a && (a.inbox || (a.endpoints && a.endpoints.sharedInbox));3135 if (!inbox) return false;3136 3136 const me = selfActorId(site.slug); 3137 3137 const keys = getOrCreateKeys(site.slug); 3138 3138 const payload = { '@context': AP_CONTEXT, ...activity }; 3139 const a = await fetchActor(actorUri).catch(() => null); 3140 const inbox = a && (a.inbox || (a.endpoints && a.endpoints.sharedInbox)); 3141 if (!inbox) { 3142 console.warn('[AP] guardianship: could not resolve an inbox for', actorUri, '(offer recorded, not sent)'); 3143 return { delivered: false, inbox: null }; 3144 } 3139 3145 try { 3140 3146 const st = await deliver(inbox, payload, `${me}#main-key`, keys.private_pem); 3141 if (st >= 200 && st < 300) return true; 3142 } catch { /* fall through to the queue */ } 3147 if (st >= 200 && st < 300) { console.log('[AP] guardianship', activity.type, 'delivered →', inbox, st); return { delivered: true, inbox }; } 3148 console.warn('[AP] guardianship', activity.type, 'got', st, 'from', inbox, '→ queued for retry'); 3149 } catch (e) { console.warn('[AP] guardianship', activity.type, 'to', inbox, 'failed:', e.message, '→ queued for retry'); } 3143 3150 enqueueDelivery(site.slug, inbox, payload); 3144 return true; // queued: it will arrive3151 return { delivered: true, inbox }; // queued: the retry worker gets it there 3145 3152 } 3146 3153 Guardianship.wireDelivery({ -
src/services/guardianship/handshake.js
r1a2d8ac rc26cc18 56 56 }; 57 57 relations.recordOffer(site.slug, 'guardian', rel.ward, { handle: deriveHandle(rel.ward), offerId }); 58 const delivered = await deliverTo(site, rel.ward, offer).catch(() => false); 58 // The offer is now recorded (the guardian sees it as pending); delivery is 59 // async + retried, so a slow ward server never fails the whole action. 60 const res = await deliverTo(site, rel.ward, offer).catch(() => ({ delivered: false })); 59 61 notify(site.slug, { kind: 'offer_sent', ward: rel.ward }); 60 return { status: delivered ? 202 : 502, id: offerId, url: offerId};62 return { status: 202, id: offerId, url: offerId, delivered: res && res.delivered !== false }; 61 63 } 62 64 … … 82 84 relations.removeRelation(site.slug, 'ward', row.other_uri); 83 85 } 84 const delivered = await deliverTo(site, row.other_uri, answer).catch(() => false); 86 // The answer is committed locally; delivery is async + retried. 87 const res = await deliverTo(site, row.other_uri, answer).catch(() => ({ delivered: false })); 85 88 notify(site.slug, { kind: type === 'Accept' ? 'offer_accepted' : 'offer_rejected', guardian: row.other_uri }); 86 return { status: delivered ? 202 : 502, id: answer.id, url: answer.id};89 return { status: 202, id: answer.id, url: answer.id, delivered: res && res.delivered !== false }; 87 90 } 88 91 -
src/services/i18n.js
r1a2d8ac rc26cc18 67 67 'admin.b_paid': 'Betaalde posts', 'admin.b_push': 'Notificaties', 'admin.back': 'Terug naar Beheer', 68 68 'push.t': 'Notificaties', 'push.intro': 'Krijg een melding op dit apparaat bij nieuwe volgers, reacties en berichten, ook als de site niet open staat. Versleuteld tot in je browser; wij sturen zo min mogelijk inhoud mee.', 'push.unavailable': 'Push is op deze server niet beschikbaar (sleutel kon niet worden aangemaakt of de dependency ontbreekt).', 'push.unsupported': 'Deze browser ondersteunt geen push-notificaties.', 'push.ios_hint': 'Op iPhone/iPad werkt dit alleen als de site op je beginscherm staat: deel-knop, dan "Zet op beginscherm", en open de site daarna vanaf daar.', 'push.this_device': 'Dit apparaat:', 'push.checking': 'controleren…', 'push.state_on': 'meldingen staan aan', 'push.state_off': 'meldingen staan uit', 'push.state_denied': 'geblokkeerd in de browserinstellingen', 'push.state_unknown': 'status onbekend', 'push.state_unsupported': 'niet ondersteund', 'push.enable': 'Zet aan op dit apparaat', 'push.disable': 'Zet uit', 'push.test': 'Stuur testmelding', 'push.what': 'Waarvoor wil je een melding?', 'push.a_follow': 'Nieuwe volger', 'push.a_reply': 'Reactie of vermelding', 'push.a_like': 'Waardering (ster)', 'push.a_boost': 'Boost', 'push.a_dm': 'Privébericht', 'push.saved': 'Opgeslagen.', 'push.devices': 'Gekoppelde apparaten', 'push.device': 'Apparaat', 'push.since': 'sinds', 'push.remove': 'Verwijder', 'push.enable_failed': 'aanzetten mislukt', 'push.on_short': 'Word supporter', 69 'push.n_follow_t': 'Nieuwe volger', 'push.n_follow_b': '{who} volgt je nu', 'push.n_reply_t': 'Reactie op "{title}"', 'push.n_mention_t': 'Vermelding', 'push.n_dm_t': 'Privébericht', 'push.n_dm_b': 'Nieuw bericht van {who}', 'push.n_like_t': 'Nieuwe waardering', 'push.n_like_b': '{who} waardeerde "{title}"', 'push.n_boost_t': 'Geboost', 'push.n_boost_b': '{who} boostte "{title}"', 'msg.guard_offer': 'wil je guardian worden. Bespreek dit met je ouders of verzorgers voordat je beslist.', 'msg.guard_accept': 'Accepteer', 'msg.guard_reject': 'Weiger', 'msg.guard_accepted': 'Guardian geaccepteerd. Jullie zijn nu verbonden.', 'msg.guard_rejected': 'Aanvraag geweigerd.', 'msg.guard_failed': 'Dat lukte niet; probeer het opnieuw.', 'guardian. help_title': 'Hulpverzoeken', 'guardian.help_empty': 'Geen hulpverzoeken. Mooi zo.', 'guardian.wards_title': 'Wards', 'guardian.adopt_label': 'Ward toevoegen', 'guardian.adopt_btn': 'Adopteer', 'guardian.wards_empty': 'Nog geen wards. Voeg er een toe met een handle.', 'guardian.push_title': 'Meldingen', 'guardian.push_sub': 'Ontvang een melding bij een hulpverzoek of voogdij-antwoord, ook als de app dicht is.', 'guardian.push_on': 'Zet meldingen aan', 'guardian.push_off': 'Meldingen staan aan; tik om uit te zetten', 'guardian.sent': 'Aanvraag verstuurd.', 'guardian.failed': 'Mislukt', 'guardian.network': 'Netwerkfout.', 'guardian.pending': 'wacht op antwoord', 'guardian.retract': 'Intrekken', 'guardian.release': 'Loslaten', 'push.n_help_t': 'Hulpvraag', 'push.n_help_b': '{who} vraagt om je hulp', 'push.n_guard_offer_t': 'Voogdij-aanvraag', 'push.n_guard_offer_b': '{who} wil je guardian worden', 'push.n_guard_ward_t': 'Ward geaccepteerd', 'push.n_guard_ward_b': '{who} accepteerde je als guardian', 'push.n_test_t': 'Klonkt-testnotificatie', 'push.n_test_b': 'Werkt. Zo komen meldingen binnen op dit apparaat.',69 'push.n_follow_t': 'Nieuwe volger', 'push.n_follow_b': '{who} volgt je nu', 'push.n_reply_t': 'Reactie op "{title}"', 'push.n_mention_t': 'Vermelding', 'push.n_dm_t': 'Privébericht', 'push.n_dm_b': 'Nieuw bericht van {who}', 'push.n_like_t': 'Nieuwe waardering', 'push.n_like_b': '{who} waardeerde "{title}"', 'push.n_boost_t': 'Geboost', 'push.n_boost_b': '{who} boostte "{title}"', 'msg.guard_offer': 'wil je guardian worden. Bespreek dit met je ouders of verzorgers voordat je beslist.', 'msg.guard_accept': 'Accepteer', 'msg.guard_reject': 'Weiger', 'msg.guard_accepted': 'Guardian geaccepteerd. Jullie zijn nu verbonden.', 'msg.guard_rejected': 'Aanvraag geweigerd.', 'msg.guard_failed': 'Dat lukte niet; probeer het opnieuw.', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards beheren en hulpverzoeken opvangen.', 'guardian.acting_as': 'Je handelt als', 'guardian.help_title': 'Hulpverzoeken', 'guardian.help_sub': 'Als een ward de reddingsboei gebruikt, verschijnt het hier.', 'guardian.help_empty': 'Geen hulpverzoeken. Mooi zo.', 'guardian.adopt_title': 'Ward adopteren', 'guardian.adopt_sub': 'Vul de handle van het kind in (@kind@server.eu). Ze krijgen een aanvraag in hun Klonkt die ze accepteren.', 'guardian.adopt_label': 'Handle van de ward', 'guardian.adopt_btn': 'Verstuur aanvraag', 'guardian.pending_title': 'Verzonden aanvragen', 'guardian.pending_sub': 'Wacht tot de ward accepteert.', 'guardian.wards_title': 'Mijn wards', 'guardian.wards_empty': 'Nog geen wards. Adopteer er hierboven een.', 'guardian.push_title': 'Meldingen', 'guardian.push_sub': 'Ontvang een melding bij een hulpverzoek of voogdij-antwoord, ook als de app dicht is.', 'guardian.push_on': 'Zet meldingen aan', 'guardian.push_off': 'Meldingen staan aan; tik om uit te zetten', 'guardian.sent': 'Aanvraag verstuurd. Zie hieronder bij Verzonden aanvragen.', 'guardian.sent_retry': 'Aanvraag opgeslagen; we blijven proberen te bezorgen.', 'guardian.sending': 'Versturen…', 'guardian.not_found': 'Die handle konden we niet vinden.', 'guardian.failed': 'Mislukt', 'guardian.network': 'Netwerkfout.', 'guardian.pending': 'wacht op antwoord', 'guardian.active': 'actief', 'guardian.retract': 'Intrekken', 'guardian.release': 'Loslaten', 'guardian.open': 'open', 'guardian.push_unavailable': 'Push niet beschikbaar', 'push.n_help_t': 'Hulpvraag', 'push.n_help_b': '{who} vraagt om je hulp', 'push.n_guard_offer_t': 'Voogdij-aanvraag', 'push.n_guard_offer_b': '{who} wil je guardian worden', 'push.n_guard_ward_t': 'Ward geaccepteerd', 'push.n_guard_ward_b': '{who} accepteerde je als guardian', 'push.n_test_t': 'Klonkt-testnotificatie', 'push.n_test_b': 'Werkt. Zo komen meldingen binnen op dit apparaat.', 70 70 'apaid.t': 'Betaalde posts', 'apaid.intro': 'Koppel je eigen Patreon-campagne. Supporters ontgrendelen betaalde posts met een passkey, zonder account en zonder cookie. Wij bewaren geen namen of e-mailadressen van supporters, alleen het versleutelde token van jouw campagne.', 'apaid.saved': 'Opgeslagen.', 'apaid.nokey': 'Let op: de encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?). Zonder sleutel kunnen secrets niet veilig worden opgeslagen.', 'apaid.status': 'Status:', 'apaid.connected': 'verbonden', 'apaid.campaign': 'campagne', 'apaid.configured': 'ingesteld, nog niet verbonden (vul een token in)', 'apaid.notyet': 'nog niet ingesteld', 'apaid.redirect_h': 'Zet deze redirect-URI in je Patreon-client', 'apaid.redirect_p': 'Bij je Patreon API-client, onder Redirect URIs, moet exact deze regel staan. Klopt hij niet, dan geeft Patreon een foutmelding in plaats van je supporters terug te sturen.', 'apaid.copy': 'Kopieer', 'apaid.copied': 'Gekopieerd', 'apaid.client_id': 'Patreon client id', 'apaid.client_secret': 'Patreon client secret', 'apaid.keep': 'Leeg laten = huidige waarde behouden.', 'apaid.campaign_id': 'Campagne-id', 'apaid.public_page': 'Openbare Patreon-pagina', 'apaid.public_help': 'De link waar bezoekers supporter kunnen worden. Getoond als "Word supporter" wanneer iemand nog niet doneert.', 'apaid.access': 'Creator access token', 'apaid.refresh': 'Creator refresh token', 'apaid.token_help': 'De access + refresh token krijg je op je Patreon API-clientpagina. Wij versleutelen ze en verversen automatisch.', 'apaid.min_eur': 'Standaard-steunbedrag voor een betaalde post (euro)', 'apaid.save': 'Opslaan', 'apaid.disconnect': 'Koppeling verwijderen', 'apaid.disconnect_confirm': 'Patreon-koppeling verwijderen?', 'apaid.unchanged': 'blijft ongewijzigd', 71 71 'pgate.h': 'Voor supporters', 'pgate.sub': 'Deze post is voor supporters van deze site. Word supporter en ontgrendel hem daarna met een passkey. Geen account op deze site, geen cookie.', 'pgate.sub_cents': 'Deze post is voor supporters van deze site (vanaf €{eur} per maand op Patreon). Word supporter en ontgrendel hem daarna met een passkey. Geen account op deze site, geen cookie.', 'pgate.join': 'Word supporter op Patreon', 'pgate.unlock_have': 'Al supporter? Ontgrendelen', 'pgate.unlock': 'Ontgrendelen met Patreon', 'pgate.join_short': 'Word supporter', 'pgate.confirm': 'Bevestig met je passkey…', 'pgate.failed': 'Ontgrendelen mislukt. Probeer opnieuw.', 'pgate.error': 'Er ging iets mis. Probeer opnieuw.', … … 1008 1008 'admin.b_paid': 'Paid posts', 'admin.b_push': 'Notifications', 'admin.back': 'Back to Admin', 1009 1009 'push.t': 'Notifications', 'push.intro': 'Get a notification on this device for new followers, replies and messages, even when the site is closed. Encrypted all the way to your browser; we send as little content as possible.', 'push.unavailable': 'Push is unavailable on this server (the key could not be created or the dependency is missing).', 'push.unsupported': 'This browser does not support push notifications.', 'push.ios_hint': 'On iPhone/iPad this only works when the site is on your home screen: share button, then "Add to Home Screen", and open it from there.', 'push.this_device': 'This device:', 'push.checking': 'checking…', 'push.state_on': 'notifications are on', 'push.state_off': 'notifications are off', 'push.state_denied': 'blocked in the browser settings', 'push.state_unknown': 'status unknown', 'push.state_unsupported': 'not supported', 'push.enable': 'Turn on for this device', 'push.disable': 'Turn off', 'push.test': 'Send a test notification', 'push.what': 'What do you want to be notified about?', 'push.a_follow': 'New follower', 'push.a_reply': 'Reply or mention', 'push.a_like': 'Like (star)', 'push.a_boost': 'Boost', 'push.a_dm': 'Private message', 'push.saved': 'Saved.', 'push.devices': 'Linked devices', 'push.device': 'Device', 'push.since': 'since', 'push.remove': 'Remove', 'push.enable_failed': 'turning on failed', 1010 'push.n_follow_t': 'New follower', 'push.n_follow_b': '{who} now follows you', 'push.n_reply_t': 'Reply to "{title}"', 'push.n_mention_t': 'Mention', 'push.n_dm_t': 'Private message', 'push.n_dm_b': 'New message from {who}', 'push.n_like_t': 'New like', 'push.n_like_b': '{who} liked "{title}"', 'push.n_boost_t': 'Boosted', 'push.n_boost_b': '{who} boosted "{title}"', 'msg.guard_offer': 'wants to become your guardian. Talk this over with your parents or carers before you decide.', 'msg.guard_accept': 'Accept', 'msg.guard_reject': 'Reject', 'msg.guard_accepted': 'Guardian accepted. You are now connected.', 'msg.guard_rejected': 'Offer rejected.', 'msg.guard_failed': 'That did not work; try again.', 'guardian. help_title': 'Help requests', 'guardian.help_empty': 'No help requests. Good.', 'guardian.wards_title': 'Wards', 'guardian.adopt_label': 'Add a ward', 'guardian.adopt_btn': 'Adopt', 'guardian.wards_empty': 'No wards yet. Add one with a handle.', 'guardian.push_title': 'Notifications', 'guardian.push_sub': 'Get notified on a call for help or a guardianship answer, even with the app closed.', 'guardian.push_on': 'Turn on notifications', 'guardian.push_off': 'Notifications are on; tap to turn off', 'guardian.sent': 'Offer sent.', 'guardian.failed': 'Failed', 'guardian.network': 'Network error.', 'guardian.pending': 'awaiting answer', 'guardian.retract': 'Retract', 'guardian.release': 'Release', 'push.n_help_t': 'Call for help', 'push.n_help_b': '{who} is asking for your help', 'push.n_guard_offer_t': 'Guardianship offer', 'push.n_guard_offer_b': '{who} wants you as their guardian', 'push.n_guard_ward_t': 'Ward accepted', 'push.n_guard_ward_b': '{who} accepted you as guardian', 'push.n_test_t': 'Klonkt test notification', 'push.n_test_b': 'It works. This is how notifications arrive on this device.',1010 'push.n_follow_t': 'New follower', 'push.n_follow_b': '{who} now follows you', 'push.n_reply_t': 'Reply to "{title}"', 'push.n_mention_t': 'Mention', 'push.n_dm_t': 'Private message', 'push.n_dm_b': 'New message from {who}', 'push.n_like_t': 'New like', 'push.n_like_b': '{who} liked "{title}"', 'push.n_boost_t': 'Boosted', 'push.n_boost_b': '{who} boosted "{title}"', 'msg.guard_offer': 'wants to become your guardian. Talk this over with your parents or carers before you decide.', 'msg.guard_accept': 'Accept', 'msg.guard_reject': 'Reject', 'msg.guard_accepted': 'Guardian accepted. You are now connected.', 'msg.guard_rejected': 'Offer rejected.', 'msg.guard_failed': 'That did not work; try again.', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Manage wards and catch calls for help.', 'guardian.acting_as': 'You act as', 'guardian.help_title': 'Help requests', 'guardian.help_sub': 'When a ward uses the help buoy, it shows up here.', 'guardian.help_empty': 'No help requests. Good.', 'guardian.adopt_title': 'Adopt a ward', 'guardian.adopt_sub': 'Enter the child handle (@kid@server.eu). They get an offer in their Klonkt to accept.', 'guardian.adopt_label': 'Ward handle', 'guardian.adopt_btn': 'Send offer', 'guardian.pending_title': 'Sent offers', 'guardian.pending_sub': 'Waiting for the ward to accept.', 'guardian.wards_title': 'My wards', 'guardian.wards_empty': 'No wards yet. Adopt one above.', 'guardian.push_title': 'Notifications', 'guardian.push_sub': 'Get notified on a call for help or a guardianship answer, even with the app closed.', 'guardian.push_on': 'Turn on notifications', 'guardian.push_off': 'Notifications are on; tap to turn off', 'guardian.sent': 'Offer sent. See it below under Sent offers.', 'guardian.sent_retry': 'Offer saved; we keep trying to deliver it.', 'guardian.sending': 'Sending…', 'guardian.not_found': 'We could not find that handle.', 'guardian.failed': 'Failed', 'guardian.network': 'Network error.', 'guardian.pending': 'awaiting answer', 'guardian.active': 'active', 'guardian.retract': 'Retract', 'guardian.release': 'Release', 'guardian.open': 'open', 'guardian.push_unavailable': 'Push unavailable', 'push.n_help_t': 'Call for help', 'push.n_help_b': '{who} is asking for your help', 'push.n_guard_offer_t': 'Guardianship offer', 'push.n_guard_offer_b': '{who} wants you as their guardian', 'push.n_guard_ward_t': 'Ward accepted', 'push.n_guard_ward_b': '{who} accepted you as guardian', 'push.n_test_t': 'Klonkt test notification', 'push.n_test_b': 'It works. This is how notifications arrive on this device.', 1011 1011 'apaid.t': 'Paid posts', 'apaid.intro': 'Connect your own Patreon campaign. Supporters unlock paid posts with a passkey, no account and no cookie. We store no supporter names or email addresses, only the encrypted token of your campaign.', 'apaid.saved': 'Saved.', 'apaid.nokey': 'Note: the encryption key could not be created or read (write permissions on the storage directory?). Without a key, secrets cannot be stored safely.', 'apaid.status': 'Status:', 'apaid.connected': 'connected', 'apaid.campaign': 'campaign', 'apaid.configured': 'configured, not connected yet (enter a token)', 'apaid.notyet': 'not configured yet', 'apaid.redirect_h': 'Put this redirect URI in your Patreon client', 'apaid.redirect_p': 'In your Patreon API client, under Redirect URIs, exactly this line must be present. If it does not match, Patreon shows an error instead of sending your supporters back.', 'apaid.copy': 'Copy', 'apaid.copied': 'Copied', 'apaid.client_id': 'Patreon client id', 'apaid.client_secret': 'Patreon client secret', 'apaid.keep': 'Leave empty = keep the current value.', 'apaid.campaign_id': 'Campaign id', 'apaid.public_page': 'Public Patreon page', 'apaid.public_help': 'The link where visitors can become a supporter. Shown as "Become a supporter" when someone does not pledge yet.', 'apaid.access': 'Creator access token', 'apaid.refresh': 'Creator refresh token', 'apaid.token_help': 'You get the access + refresh token on your Patreon API client page. We encrypt them and refresh automatically.', 'apaid.min_eur': 'Default support amount for a paid post (euro)', 'apaid.save': 'Save', 'apaid.disconnect': 'Remove connection', 'apaid.disconnect_confirm': 'Remove the Patreon connection?', 'apaid.unchanged': 'stays unchanged', 1012 1012 'pgate.h': 'For supporters', 'pgate.sub': 'This post is for supporters of this site. Become a supporter and then unlock it with a passkey. No account on this site, no cookie.', 'pgate.sub_cents': 'This post is for supporters of this site (from €{eur} per month on Patreon). Become a supporter and then unlock it with a passkey. No account on this site, no cookie.', 'pgate.join': 'Become a supporter on Patreon', 'pgate.unlock_have': 'Already a supporter? Unlock', 'pgate.unlock': 'Unlock with Patreon', 'pgate.join_short': 'Become a supporter', 'pgate.confirm': 'Confirm with your passkey…', 'pgate.failed': 'Unlocking failed. Try again.', 'pgate.error': 'Something went wrong. Try again.', … … 1943 1943 'admin.b_paid': 'Bezahlte Beiträge', 'admin.b_push': 'Benachrichtigungen', 'admin.back': 'Zurück zur Verwaltung', 1944 1944 'push.t': 'Benachrichtigungen', 'push.intro': 'Erhalte auf diesem Gerät eine Meldung bei neuen Followern, Antworten und Nachrichten, auch wenn die Seite geschlossen ist. Verschlüsselt bis in deinen Browser; wir senden so wenig Inhalt wie möglich mit.', 'push.unavailable': 'Push ist auf diesem Server nicht verfügbar (Schlüssel konnte nicht erstellt werden oder die Abhängigkeit fehlt).', 'push.unsupported': 'Dieser Browser unterstützt keine Push-Benachrichtigungen.', 'push.ios_hint': 'Auf iPhone/iPad funktioniert das nur, wenn die Seite auf deinem Home-Bildschirm liegt: Teilen-Knopf, dann "Zum Home-Bildschirm", und öffne sie danach von dort.', 'push.this_device': 'Dieses Gerät:', 'push.checking': 'prüfen…', 'push.state_on': 'Benachrichtigungen sind an', 'push.state_off': 'Benachrichtigungen sind aus', 'push.state_denied': 'in den Browser-Einstellungen blockiert', 'push.state_unknown': 'Status unbekannt', 'push.state_unsupported': 'nicht unterstützt', 'push.enable': 'Auf diesem Gerät einschalten', 'push.disable': 'Ausschalten', 'push.test': 'Testmeldung senden', 'push.what': 'Wofür möchtest du eine Meldung?', 'push.a_follow': 'Neuer Follower', 'push.a_reply': 'Antwort oder Erwähnung', 'push.a_like': 'Like (Stern)', 'push.a_boost': 'Boost', 'push.a_dm': 'Private Nachricht', 'push.saved': 'Gespeichert.', 'push.devices': 'Verbundene Geräte', 'push.device': 'Gerät', 'push.since': 'seit', 'push.remove': 'Entfernen', 'push.enable_failed': 'Einschalten fehlgeschlagen', 1945 'push.n_follow_t': 'Neuer Follower', 'push.n_follow_b': '{who} folgt dir jetzt', 'push.n_reply_t': 'Antwort auf "{title}"', 'push.n_mention_t': 'Erwähnung', 'push.n_dm_t': 'Private Nachricht', 'push.n_dm_b': 'Neue Nachricht von {who}', 'push.n_like_t': 'Neues Like', 'push.n_like_b': '{who} gefällt "{title}"', 'push.n_boost_t': 'Geboostet', 'push.n_boost_b': '{who} hat "{title}" geboostet', 'msg.guard_offer': 'möchte dein Guardian werden. Besprich das mit deinen Eltern oder Betreuern, bevor du entscheidest.', 'msg.guard_accept': 'Annehmen', 'msg.guard_reject': 'Ablehnen', 'msg.guard_accepted': 'Guardian angenommen. Ihr seid jetzt verbunden.', 'msg.guard_rejected': 'Angebot abgelehnt.', 'msg.guard_failed': 'Das hat nicht geklappt; versuch es erneut.', 'guardian. help_title': 'Hilferufe', 'guardian.help_empty': 'Keine Hilferufe. Gut so.', 'guardian.wards_title': 'Wards', 'guardian.adopt_label': 'Ward hinzufügen', 'guardian.adopt_btn': 'Adoptieren', 'guardian.wards_empty': 'Noch keine Wards. Füge eins per Handle hinzu.', 'guardian.push_title': 'Meldungen', 'guardian.push_sub': 'Erhalte eine Meldung bei einem Hilferuf oder einer Vormundschafts-Antwort, auch bei geschlossener App.', 'guardian.push_on': 'Meldungen einschalten', 'guardian.push_off': 'Meldungen sind an; tippen zum Ausschalten', 'guardian.sent': 'Angebot gesendet.', 'guardian.failed': 'Fehlgeschlagen', 'guardian.network': 'Netzwerkfehler.', 'guardian.pending': 'wartet auf Antwort', 'guardian.retract': 'Zurückziehen', 'guardian.release': 'Loslassen', 'push.n_help_t': 'Hilferuf', 'push.n_help_b': '{who} bittet um deine Hilfe', 'push.n_guard_offer_t': 'Vormundschaftsangebot', 'push.n_guard_offer_b': '{who} möchte dich als Guardian', 'push.n_guard_ward_t': 'Ward akzeptiert', 'push.n_guard_ward_b': '{who} hat dich als Guardian akzeptiert', 'push.n_test_t': 'Klonkt-Testmeldung', 'push.n_test_b': 'Funktioniert. So kommen Meldungen auf diesem Gerät an.',1945 'push.n_follow_t': 'Neuer Follower', 'push.n_follow_b': '{who} folgt dir jetzt', 'push.n_reply_t': 'Antwort auf "{title}"', 'push.n_mention_t': 'Erwähnung', 'push.n_dm_t': 'Private Nachricht', 'push.n_dm_b': 'Neue Nachricht von {who}', 'push.n_like_t': 'Neues Like', 'push.n_like_b': '{who} gefällt "{title}"', 'push.n_boost_t': 'Geboostet', 'push.n_boost_b': '{who} hat "{title}" geboostet', 'msg.guard_offer': 'möchte dein Guardian werden. Besprich das mit deinen Eltern oder Betreuern, bevor du entscheidest.', 'msg.guard_accept': 'Annehmen', 'msg.guard_reject': 'Ablehnen', 'msg.guard_accepted': 'Guardian angenommen. Ihr seid jetzt verbunden.', 'msg.guard_rejected': 'Angebot abgelehnt.', 'msg.guard_failed': 'Das hat nicht geklappt; versuch es erneut.', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards verwalten und Hilferufe auffangen.', 'guardian.acting_as': 'Du handelst als', 'guardian.help_title': 'Hilferufe', 'guardian.help_sub': 'Wenn ein Ward die Rettungsboje nutzt, erscheint es hier.', 'guardian.help_empty': 'Keine Hilferufe. Gut so.', 'guardian.adopt_title': 'Ward adoptieren', 'guardian.adopt_sub': 'Gib das Handle des Kindes ein (@kind@server.eu). Es bekommt ein Angebot in seinem Klonkt zum Annehmen.', 'guardian.adopt_label': 'Ward-Handle', 'guardian.adopt_btn': 'Angebot senden', 'guardian.pending_title': 'Gesendete Angebote', 'guardian.pending_sub': 'Warten, bis der Ward annimmt.', 'guardian.wards_title': 'Meine Wards', 'guardian.wards_empty': 'Noch keine Wards. Adoptiere oben eins.', 'guardian.push_title': 'Meldungen', 'guardian.push_sub': 'Erhalte eine Meldung bei einem Hilferuf oder einer Vormundschafts-Antwort, auch bei geschlossener App.', 'guardian.push_on': 'Meldungen einschalten', 'guardian.push_off': 'Meldungen sind an; tippen zum Ausschalten', 'guardian.sent': 'Angebot gesendet. Siehe unten bei Gesendete Angebote.', 'guardian.sent_retry': 'Angebot gespeichert; wir versuchen weiter zuzustellen.', 'guardian.sending': 'Senden…', 'guardian.not_found': 'Dieses Handle konnten wir nicht finden.', 'guardian.failed': 'Fehlgeschlagen', 'guardian.network': 'Netzwerkfehler.', 'guardian.pending': 'wartet auf Antwort', 'guardian.active': 'aktiv', 'guardian.retract': 'Zurückziehen', 'guardian.release': 'Loslassen', 'guardian.open': 'öffnen', 'guardian.push_unavailable': 'Push nicht verfügbar', 'push.n_help_t': 'Hilferuf', 'push.n_help_b': '{who} bittet um deine Hilfe', 'push.n_guard_offer_t': 'Vormundschaftsangebot', 'push.n_guard_offer_b': '{who} möchte dich als Guardian', 'push.n_guard_ward_t': 'Ward akzeptiert', 'push.n_guard_ward_b': '{who} hat dich als Guardian akzeptiert', 'push.n_test_t': 'Klonkt-Testmeldung', 'push.n_test_b': 'Funktioniert. So kommen Meldungen auf diesem Gerät an.', 1946 1946 'apaid.t': 'Bezahlte Beiträge', 'apaid.intro': 'Verbinde deine eigene Patreon-Kampagne. Unterstützer entsperren bezahlte Beiträge mit einem Passkey, ohne Konto und ohne Cookie. Wir speichern keine Namen oder E-Mail-Adressen von Unterstützern, nur das verschlüsselte Token deiner Kampagne.', 'apaid.saved': 'Gespeichert.', 'apaid.nokey': 'Achtung: der Verschlüsselungsschlüssel konnte nicht erstellt oder gelesen werden (Schreibrechte auf dem Speicherordner?). Ohne Schlüssel können Secrets nicht sicher gespeichert werden.', 'apaid.status': 'Status:', 'apaid.connected': 'verbunden', 'apaid.campaign': 'Kampagne', 'apaid.configured': 'eingerichtet, noch nicht verbunden (Token eintragen)', 'apaid.notyet': 'noch nicht eingerichtet', 'apaid.redirect_h': 'Trage diese Redirect-URI in deinen Patreon-Client ein', 'apaid.redirect_p': 'In deinem Patreon-API-Client muss unter Redirect URIs genau diese Zeile stehen. Stimmt sie nicht, zeigt Patreon eine Fehlermeldung statt deine Unterstützer zurückzuschicken.', 'apaid.copy': 'Kopieren', 'apaid.copied': 'Kopiert', 'apaid.client_id': 'Patreon Client-ID', 'apaid.client_secret': 'Patreon Client-Secret', 'apaid.keep': 'Leer lassen = aktuellen Wert behalten.', 'apaid.campaign_id': 'Kampagnen-ID', 'apaid.public_page': 'Öffentliche Patreon-Seite', 'apaid.public_help': 'Der Link, unter dem Besucher Unterstützer werden können. Wird als "Unterstützer werden" gezeigt, wenn jemand noch nicht spendet.', 'apaid.access': 'Creator Access-Token', 'apaid.refresh': 'Creator Refresh-Token', 'apaid.token_help': 'Access- und Refresh-Token bekommst du auf deiner Patreon-API-Client-Seite. Wir verschlüsseln sie und erneuern automatisch.', 'apaid.min_eur': 'Standard-Unterstützungsbetrag für einen bezahlten Beitrag (Euro)', 'apaid.save': 'Speichern', 'apaid.disconnect': 'Verbindung entfernen', 'apaid.disconnect_confirm': 'Patreon-Verbindung entfernen?', 'apaid.unchanged': 'bleibt unverändert', 1947 1947 'pgate.h': 'Für Unterstützer', 'pgate.sub': 'Dieser Beitrag ist für Unterstützer dieser Seite. Werde Unterstützer und entsperre ihn danach mit einem Passkey. Kein Konto auf dieser Seite, kein Cookie.', 'pgate.sub_cents': 'Dieser Beitrag ist für Unterstützer dieser Seite (ab €{eur} pro Monat auf Patreon). Werde Unterstützer und entsperre ihn danach mit einem Passkey. Kein Konto auf dieser Seite, kein Cookie.', 'pgate.join': 'Unterstützer werden auf Patreon', 'pgate.unlock_have': 'Schon Unterstützer? Entsperren', 'pgate.unlock': 'Mit Patreon entsperren', 'pgate.join_short': 'Unterstützer werden', 'pgate.confirm': 'Bestätige mit deinem Passkey…', 'pgate.failed': 'Entsperren fehlgeschlagen. Versuch es erneut.', 'pgate.error': 'Etwas ist schiefgegangen. Versuch es erneut.', -
src/views/pages/guardian.ejs
r1a2d8ac rc26cc18 4 4 <meta charset="utf-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title> Klonkt Guardian</title>6 <title><%= t('guardian.app_name') %></title> 7 7 <link rel="manifest" href="/guardian/manifest.webmanifest"> 8 8 <link rel="icon" href="/guardian/icon.svg" type="image/svg+xml"> … … 13 13 <header class="g-head"> 14 14 <span class="g-buoy">🛟</span> 15 <h1>Guardian</h1> 15 <div class="g-head-txt"> 16 <h1><%= t('guardian.app_name') %></h1> 17 <p class="g-sub"><%= t('guardian.tagline') %></p> 18 </div> 16 19 <% if (sites.length > 1) { %> 17 <select id="site-picker" aria-label=" Account">20 <select id="site-picker" aria-label="<%= t('guardian.acting_as') %>"> 18 21 <% for (const s of sites) { %> 19 22 <option value="<%= s.slug %>" <%= s.slug === state.site ? 'selected' : '' %>>@<%= s.slug %></option> … … 21 24 </select> 22 25 <% } else { %> 23 <span class="g-me" >@<%= state.site %></span>26 <span class="g-me" title="<%= t('guardian.acting_as') %>">@<%= state.site %></span> 24 27 <% } %> 25 28 </header> 26 29 27 30 <main> 28 <!-- Berichtencentrum: hulpverzoeken eerst, dat is waar dit voor bestaat. -->31 <!-- 1. Berichtencentrum: hulpverzoeken. Dat is waar dit voor bestaat. --> 29 32 <section id="help-section"> 30 <h2><%= t('guardian.help_title') %></h2> 33 <div class="g-sec-head"> 34 <h2><%= t('guardian.help_title') %></h2> 35 <span id="help-count" class="g-badge" hidden></span> 36 </div> 37 <p class="g-sec-sub"><%= t('guardian.help_sub') %></p> 31 38 <div id="help-list" class="g-list"></div> 32 <p id="help-empty" class="g-empty" hidden><%= t('guardian.help_empty') %></p>39 <p id="help-empty" class="g-empty"><%= t('guardian.help_empty') %></p> 33 40 </section> 34 41 35 <section id="wards-section"> 36 <h2><%= t('guardian.wards_title') %></h2> 42 <!-- 2. Nieuwe ward adopteren. --> 43 <section id="adopt-section"> 44 <div class="g-sec-head"><h2><%= t('guardian.adopt_title') %></h2></div> 45 <p class="g-sec-sub"><%= t('guardian.adopt_sub') %></p> 37 46 <form id="adopt-form" autocomplete="off"> 38 <input id="adopt-handle" type="text" inputmode="email" 47 <input id="adopt-handle" type="text" inputmode="email" autocomplete="off" 39 48 placeholder="@kind@server.eu" aria-label="<%= t('guardian.adopt_label') %>"> 40 <button type="submit" ><%= t('guardian.adopt_btn') %></button>49 <button type="submit" id="adopt-btn"><%= t('guardian.adopt_btn') %></button> 41 50 </form> 42 51 <p id="adopt-msg" class="g-msg" hidden></p> 43 <div id="wards-list" class="g-list"></div>44 <div id="offers-list" class="g-list"></div>45 <p id="wards-empty" class="g-empty" hidden><%= t('guardian.wards_empty') %></p>46 52 </section> 47 53 54 <!-- 3. Verzonden aanvragen (nog niet beantwoord). --> 55 <section id="pending-section" hidden> 56 <div class="g-sec-head"><h2><%= t('guardian.pending_title') %></h2></div> 57 <p class="g-sec-sub"><%= t('guardian.pending_sub') %></p> 58 <div id="pending-list" class="g-list"></div> 59 </section> 60 61 <!-- 4. Mijn wards (geaccepteerd). --> 62 <section id="wards-section"> 63 <div class="g-sec-head"><h2><%= t('guardian.wards_title') %></h2></div> 64 <div id="wards-list" class="g-list"></div> 65 <p id="wards-empty" class="g-empty"><%= t('guardian.wards_empty') %></p> 66 </section> 67 68 <!-- 5. Meldingen. --> 48 69 <section id="push-section"> 49 < h2><%= t('guardian.push_title') %></h2>50 <p class="g-s ub"><%= t('guardian.push_sub') %></p>51 <button id="push-toggle" data-on-label="<%= t('guardian.push_off') %>"70 <div class="g-sec-head"><h2><%= t('guardian.push_title') %></h2></div> 71 <p class="g-sec-sub"><%= t('guardian.push_sub') %></p> 72 <button id="push-toggle" class="quiet" data-on-label="<%= t('guardian.push_off') %>" 52 73 data-off-label="<%= t('guardian.push_on') %>"><%= t('guardian.push_on') %></button> 53 74 <p id="push-msg" class="g-msg" hidden></p>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)