Changeset f1c50f9 in Klonkt
- Timestamp:
- 07/25/2026 10:37:37 AM (7 weeks ago)
- Branches:
- main
- Children:
- 97bacf2
- Parents:
- 787e0d9
- Location:
- src
- Files:
-
- 4 deleted
- 7 edited
-
assets/css/guardian.css (modified) (1 diff)
-
assets/css/guardian2.css (deleted)
-
assets/js/guardian.js (modified) (4 diffs)
-
assets/js/guardian2.js (deleted)
-
routes/guardian.js (modified) (4 diffs)
-
routes/guardian2.js (deleted)
-
server.js (modified) (2 diffs)
-
services/ActivityPubService.js (modified) (4 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/guardian.ejs (modified) (1 diff)
-
views/pages/guardian2.ejs (deleted)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/guardian.css
r787e0d9 rf1c50f9 83 83 button.quiet.is-on { color: var(--ok); border-color: var(--ok); } 84 84 button.small { padding: 6px 12px; font-size: .82rem; } 85 86 /* Wards' corner (v2 meekijken) */ 87 .g-card.feed .g-when { color: var(--muted, #888); font-size: .8em; margin-left: auto; } 88 .g-card.feed .feed-body { margin-top: 6px; line-height: 1.4; } 89 .g-card.feed .feed-body img { max-width: 100%; border-radius: 8px; } -
src/assets/js/guardian.js
r787e0d9 rf1c50f9 114 114 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 115 115 row.appendChild(el('span', 'tag ok', T.active)); 116 var wave = el('button', 'small', T.wave || '👋 Wave'); 117 wave.addEventListener('click', function () { sendWave(w.other_uri, wave); }); 118 row.appendChild(wave); 116 119 var btn = el('button', 'quiet small', T.release); 117 120 btn.addEventListener('click', function () { remove(w.other_uri, btn); }); … … 123 126 } 124 127 128 function sendWave(uri, btn) { 129 btn.disabled = true; 130 fetch('/guardian/api/wave', { 131 method: 'POST', headers: { 'Content-Type': 'application/json' }, 132 body: JSON.stringify({ ward: uri, site: S.site }), 133 }).then(function (r) { return r.json(); }) 134 .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '👋 sent') : (T.wave || '👋 Wave'); }) 135 .catch(function () { btn.disabled = false; }); 136 } 137 125 138 function remove(uri, btn) { 126 139 btn.disabled = true; … … 133 146 function renderAll() { renderHelp(); renderPending(); renderWards(); } 134 147 148 // ── 0. Wards' corner: read-only feed of your wards' posts ─────────────── 149 function renderFeed(items) { 150 var list = document.getElementById('feed-list'); 151 list.textContent = ''; 152 (items || []).forEach(function (p) { 153 var card = el('div', 'g-card feed'); 154 var head = el('div', 'row'); 155 head.appendChild(el('span', 'who grow', p.author)); 156 if (p.published) head.appendChild(el('span', 'g-when', when(p.published))); 157 card.appendChild(head); 158 var body = el('div', 'feed-body'); 159 if (p.cw) { 160 var d = document.createElement('details'); 161 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum); 162 var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner); 163 body.appendChild(d); 164 } else { 165 body.innerHTML = p.content || ''; // server-sanitized HTML (same as Berichten) 166 } 167 card.appendChild(body); 168 list.appendChild(card); 169 }); 170 show('feed-section', (items || []).length > 0); 171 } 172 173 function loadFeed() { 174 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site)) 175 .then(function (r) { return r.json(); }) 176 .then(function (f) { if (f && !f.error) renderFeed(f.items); }) 177 .catch(function () { /* corner just stays hidden */ }); 178 } 179 180 // ── 0b. Follow requests on your wards (§5.3) ──────────────────────────── 181 function answerFollow(id, decision, btn) { 182 if (btn) btn.disabled = true; 183 fetch('/guardian/api/follow/' + encodeURIComponent(id), { 184 method: 'POST', headers: { 'Content-Type': 'application/json' }, 185 body: JSON.stringify({ decision: decision, site: S.site }), 186 }).then(loadFollowReqs); 187 } 188 function renderFollowReqs(items) { 189 var list = document.getElementById('follow-list'); 190 list.textContent = ''; 191 (items || []).forEach(function (f) { 192 var card = el('div', 'g-card'); 193 var row = el('div', 'row'); 194 row.appendChild(el('span', 'who grow', f.follower + ' → ' + f.ward)); 195 var ok = el('button', 'small', T.accept || 'Accept'); 196 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); }); 197 var no = el('button', 'quiet small', T.reject || 'Deny'); 198 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); }); 199 row.appendChild(ok); row.appendChild(no); 200 card.appendChild(row); 201 list.appendChild(card); 202 }); 203 show('follow-section', (items || []).length > 0); 204 } 205 function loadFollowReqs() { 206 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site)) 207 .then(function (r) { return r.json(); }) 208 .then(function (f) { if (f && !f.error) renderFollowReqs(f.items); }) 209 .catch(function () { /* stays hidden */ }); 210 } 211 135 212 function refresh() { 136 213 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site)) 137 214 .then(function (r) { return r.json(); }) 138 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } }); 215 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } }) 216 .then(loadFeed).then(loadFollowReqs); 139 217 } 140 218 … … 230 308 }); 231 309 232 renderAll(); pushState(); 310 renderAll(); pushState(); loadFeed(); loadFollowReqs(); 233 311 setInterval(refresh, 45000); // live-ish while open 234 312 } catch (e) { -
src/routes/guardian.js
r787e0d9 rf1c50f9 10 10 */ 11 11 import express from 'express'; 12 import crypto from 'crypto'; 13 import bcrypt from 'bcryptjs'; 12 14 import path from 'path'; 13 15 import { fileURLToPath } from 'url'; … … 38 40 'pending', 'active', 'retract', 'release', 'open', 'push_unavailable', 39 41 'accept', 'reject', 'complete', 'awaiting_others', 'coguard']; 40 return Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)])); 42 const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)])); 43 s.wave = i18nT(L, 'guardian.wave'); 44 s.waved = i18nT(L, 'guardian.waved'); 45 return s; 41 46 } 42 47 … … 84 89 if (!site) return res.status(404).json({ error: 'no_site' }); 85 90 res.json(dashboardState(site, resolveLang(req))); 91 }); 92 93 // ── Meekijken (FEP-633c §5, interop-hoofdroute): a committed guardian FOLLOWS 94 // its wards, so their posts (incl. followers-only) are DELIVERED to the 95 // guardian's inbox → timeline. The follow is the mechanism; no new fetch. 96 // First contact also backfills the ward's recent PUBLIC posts as a cold 97 // start so the corner is not empty before delivery catches up. 98 function ensureWardConnections(site) { 99 let wards; 100 try { wards = Guardianship.listWards(site.slug); } catch { return; } 101 for (const w of wards) { 102 const already = db.prepare('SELECT 1 FROM ap_following WHERE slug = ? AND actor_uri = ?') 103 .get(site.slug, w.other_uri); 104 if (already) continue; 105 // Follow (guardian's server auto-accepts today; §5.3 gating is a later fase). 106 AP.followActor(site, w.other_uri).catch(() => { /* retried by the queue */ }); 107 // Cold start: pull recent public posts now so oma sees something at once. 108 AP.backfillFromOutbox(site.slug, w.other_uri).catch(() => { /* best-effort */ }); 109 } 110 } 111 112 // ── The wards' corner: your wards' posts, read-only. No reply, no share; a 113 // guardian watches, it does not publish (Robins besluit). 114 router.get('/api/feed', requireAuth, (req, res) => { 115 const site = siteForUser(req); 116 if (!site) return res.status(404).json({ error: 'no_site' }); 117 ensureWardConnections(site); 118 const wardUris = new Set(Guardianship.listWards(site.slug).map((w) => w.other_uri)); 119 // Only show the wards you actually guard (the timeline can hold more). 120 const items = AP.getTimeline(site.slug, 60, 0) 121 .filter((p) => wardUris.has(p.author_uri)) 122 .map((p) => ({ 123 id: p.id, 124 author: p.author_handle || p.author_name || p.author_uri, 125 authorName: p.author_name, 126 authorIcon: p.author_icon, 127 content: p.content, 128 url: p.url, 129 published: p.published || p.created_at, 130 cw: p.cw || null, 131 media: p.media_json ? JSON.parse(p.media_json) : [], 132 })); 133 res.json({ items, following: wardUris.size }); 134 }); 135 136 // ── Follow-gating (FEP-633c §5.3): pending follows on MY wards, for me to 137 // approve. Ward and guardian are co-located on the family Klonkt here, so 138 // the guardian reads its wards' pending follows locally. 139 function wardSlugsOf(site) { 140 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 141 return Guardianship.listWards(site.slug) 142 .map((w) => (w.other_uri.startsWith(base) ? w.other_uri.split('/').pop() : null)) 143 .filter(Boolean); 144 } 145 146 router.get('/api/follow-requests', requireAuth, (req, res) => { 147 const site = siteForUser(req); 148 if (!site) return res.status(404).json({ error: 'no_site' }); 149 const items = []; 150 const host = (() => { try { return new URL(process.env.PUBLIC_BASE_URL || '').host; } catch { return ''; } })(); 151 // Local wards (guardian co-located): read the pending follows directly. 152 for (const wardSlug of wardSlugsOf(site)) { 153 for (const f of Guardianship.follows.listForWard(wardSlug)) { 154 items.push({ id: f.id, ward: `@${wardSlug}@${host}`, follower: f.follower_handle || f.follower_name || f.follower_uri, followerIcon: f.follower_icon, remote: false, created: f.created_at }); 155 } 156 } 157 // Remote wards: the copies forwarded here as Offer(Follow) (cross-instance). 158 for (const rev of Guardianship.follows.listReviews(site.slug)) { 159 const wardName = (() => { try { const u = new URL(rev.ward_uri); return `@${u.pathname.split('/').pop()}@${u.host}`; } catch { return rev.ward_uri; } })(); 160 items.push({ id: rev.id, ward: wardName, follower: rev.follower_handle || rev.follower_uri, followerIcon: rev.follower_icon, remote: true, created: rev.created_at }); 161 } 162 res.json({ items }); 163 }); 164 165 router.post('/api/follow/:id', requireAuth, express.json({ limit: '4kb' }), async (req, res) => { 166 const site = siteForUser(req); 167 if (!site) return res.status(404).json({ error: 'no_site' }); 168 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 169 const me = AP.actorId(base, site.slug); 170 const decision = req.body?.decision === 'reject' ? 'reject' : 'approve'; 171 172 // Remote ward: a forwarded copy. Send my Accept/Reject back to the ward, 173 // which tallies quorum and returns the Accept(Follow) to the follower. 174 const review = Guardianship.follows.getReview(site.slug, req.params.id); 175 if (review) { 176 try { await AP.sendFollowDecision(site, review, decision); } 177 catch { return res.status(502).json({ error: 'delivery' }); } 178 Guardianship.follows.removeReview(site.slug, req.params.id); 179 return res.json({ ok: true, outcome: decision === 'reject' ? 'rejected' : 'sent' }); 180 } 181 182 // Local ward: decide directly (quorum on this instance). 183 const pending = Guardianship.follows.getPending(req.params.id); 184 if (!pending) return res.status(404).json({ error: 'gone' }); 185 const guardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri); 186 if (!guardians.includes(me)) return res.status(403).json({ error: 'not_a_guardian' }); 187 const r = Guardianship.follows.decide(pending.id, me, decision, guardians); 188 try { 189 if (r.outcome === 'approved') { await AP.acceptGatedFollow(r.follow); Guardianship.follows.remove(r.follow.id); } 190 else if (r.outcome === 'rejected') { await AP.rejectGatedFollow(r.follow); Guardianship.follows.remove(r.follow.id); } 191 } catch (e) { return res.status(502).json({ error: 'delivery', outcome: r.outcome }); } 192 res.json({ ok: true, outcome: r.outcome }); 193 }); 194 195 // ── Wave (FEP-633c §5, shaer:wave): a gentle "thinking of you" from a 196 // guardian to a ward. A private direct note, never a feed post. Warmth 197 // without publishing (Robins besluit). 198 router.post('/api/wave', requireAuth, express.json({ limit: '2kb' }), async (req, res) => { 199 const site = siteForUser(req); 200 if (!site) return res.status(404).json({ error: 'no_site' }); 201 const wardUri = String(req.body?.ward || '').trim(); 202 // Only wave at a ward you actually guard. 203 const isWard = Guardianship.listWards(site.slug).some((w) => w.other_uri === wardUri); 204 if (!wardUri || !isWard) return res.status(403).json({ error: 'not_your_ward' }); 205 const text = String(req.body?.text || '').trim().slice(0, 200) || '👋 thinking of you'; 206 const r = await AP.deliverDirectNote(site, { recipients: [wardUri], text, wave: true }).catch(() => null); 207 if (!r) return res.status(502).json({ error: 'delivery' }); 208 res.json({ ok: true, delivered: r.delivered }); 86 209 }); 87 210 … … 181 304 }); 182 305 306 // ── Losse guardians (Guardian 2): uitnodigen en aansluiten ─────────────── 307 // De familie nodigt oma uit; zij kiest naam + wachtwoord en heeft daarmee een 308 // guardian-only account: user + minimale site (guardian_only=1). Alles wat al 309 // per slug werkt (actor, inbox, offers, push, deze PWA) werkt dan meteen. 310 311 router.post('/invite', requireAuth, (req, res) => { 312 const token = crypto.randomBytes(16).toString('base64url'); 313 db.prepare('INSERT INTO ap_guardian_invites (token, created_by) VALUES (?,?)') 314 .run(token, req.session.user.id); 315 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 316 const url = `${base}/guardian/join/${token}`; 317 res.send(`<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif;max-width:480px;margin:40px auto"> 318 <h2>Invite a guardian</h2> 319 <p>Share this link. It lets one person create a guardian account here:</p> 320 <p><a href="${url}">${url}</a></p> 321 <p><a href="/guardian">Back</a></p></body>`); 322 }); 323 324 function joinForm(token, error) { 325 return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> 326 <body style="font-family:sans-serif;max-width:420px;margin:40px auto"> 327 <h2>Become a guardian</h2> 328 <p>Watch over someone you care about. Pick a name and a password; that is all.</p> 329 ${error ? `<p style="color:#b00">${error}</p>` : ''} 330 <form method="post" action="/guardian/join/${token}"> 331 <p><input name="name" placeholder="your name (grandma)" required pattern="[a-z0-9_-]{1,32}" 332 style="width:100%;padding:10px" autocapitalize="none"></p> 333 <p><input name="password" type="password" placeholder="password" required minlength="8" 334 style="width:100%;padding:10px"></p> 335 <p><button style="width:100%;padding:12px">Create my guardian account</button></p> 336 </form></body>`; 337 } 338 339 router.get('/join/:token', (req, res) => { 340 const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL') 341 .get(req.params.token); 342 if (!inv) return res.status(404).send('This invite is no longer valid.'); 343 res.send(joinForm(req.params.token)); 344 }); 345 346 router.post('/join/:token', express.urlencoded({ extended: false }), (req, res) => { 347 const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL') 348 .get(req.params.token); 349 if (!inv) return res.status(404).send('This invite is no longer valid.'); 350 const name = String(req.body.name || '').trim().toLowerCase(); 351 const password = String(req.body.password || ''); 352 if (!/^[a-z0-9_-]{1,32}$/.test(name)) return res.status(400).send(joinForm(req.params.token, 'Only lowercase letters, digits, - and _.')); 353 if (password.length < 8) return res.status(400).send(joinForm(req.params.token, 'Password: at least 8 characters.')); 354 if (db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(name) || db.prepare('SELECT 1 FROM users WHERE username = ?').get(name)) { 355 return res.status(409).send(joinForm(req.params.token, 'That name is taken, pick another.')); 356 } 357 const userId = crypto.randomUUID(); 358 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)') 359 .run(userId, name, `${name}@guardian.invalid`, bcrypt.hashSync(password, 10), 'member'); 360 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, guardian_only) VALUES (?,?,?,?,0,1)') 361 .run(crypto.randomUUID(), name, name, userId); 362 db.prepare('UPDATE ap_guardian_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE token = ?') 363 .run(userId, req.params.token); 364 req.session.user = { id: userId, username: name, role: 'member' }; 365 res.redirect('/guardian'); 366 }); 367 183 368 export default router; -
src/server.js
r787e0d9 rf1c50f9 50 50 import pushRoutes from './routes/push.js'; 51 51 import guardianRoutes from './routes/guardian.js'; 52 import guardian2Routes from './routes/guardian2.js';53 52 import adminMediaRoutes from './routes/admin-media.js'; 54 53 import circleRoutes from './routes/circle.js'; … … 408 407 app.use('/paid', paidRoutes); // paid-posts patron/passkey flow (before the /:slug catch-all) 409 408 app.use('/push', pushRoutes); // web-push subscribe/test (before the /:slug catch-all) 410 app.use('/guardian', guardianRoutes); // the Guardian PWA (FEP-633c, before the /:slug catch-all) 411 app.use('/guardian2', guardian2Routes); // Guardian v2: losse guardians (guardian-only accounts), groeit hier los van v1 409 app.use('/guardian', guardianRoutes); // the Guardian PWA (FEP-633c): losse guardians, meekijken, follow-gating, wave, invite (was guardian2, v1 verwijderd) 412 410 app.use('/', postsRoutes); 413 411 -
src/services/ActivityPubService.js
r787e0d9 rf1c50f9 1405 1405 // FEP-633c §5.3, modelled on the guardian offer: the ward forwards the 1406 1406 // gated follow to its guardians for approval. A LOCAL guardian gets a 1407 // push and reads /guardian 2directly; a REMOTE guardian gets an1407 // push and reads /guardian directly; a REMOTE guardian gets an 1408 1408 // Offer(Follow) delivered so its instance stores a copy (same distributed 1409 1409 // pattern as the adoption offer). On quorum the ward returns Accept(Follow). … … 1419 1419 if (isLocal) { 1420 1420 const L = pushLang(gslug); 1421 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fi.name || fi.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian 2` });1421 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fi.name || fi.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian` }); 1422 1422 } else { 1423 1423 fetchActor(g).then((ga) => { … … 3009 3009 Guardianship.follows.recordReview(gslug, { id: followId, wardUri, wardInbox: wardDoc && wardDoc.inbox, follower, followerHandle: fai.handle, followerIcon: fai.icon, followJson: JSON.stringify(fo) }); 3010 3010 const L = pushLang(gslug); 3011 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fai.name || fai.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian 2` });3011 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fai.name || fai.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian` }); 3012 3012 stored = true; 3013 3013 } … … 3032 3032 } 3033 3033 3034 // Leg 3: a guardian in /guardian 2decides on a forwarded follow; send the3034 // Leg 3: a guardian in /guardian decides on a forwarded follow; send the 3035 3035 // Accept/Reject back to the ward's inbox (signed by the guardian). 3036 3036 export async function sendFollowDecision(guardianSite, review, decision) { -
src/services/i18n.js
r787e0d9 rf1c50f9 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.', 'msg.guardians_label': 'Jouw guardians', 'msg.waved_at_you': 'zwaaide naar je', 'msg.wave_r1': 'Wat leuk!', 'msg.wave_r2': 'Bel me even', 'msg.wave_back': '👋 Terug', 'msg.wave_sent': 'Zwaai verstuurd.', 'guardian 2.feed_title': 'Van je wards', 'guardian2.feed_sub': 'Meelezen met wat je wards plaatsen. Alleen kijken.', 'guardian2.follow_title': 'Volgverzoeken', 'guardian2.follow_sub': 'Iemand wil een van je wards volgen. Jij beslist.', 'guardian2.wave': '👋 Zwaai', 'guardian2.waved': '👋 verstuurd', '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.accept': 'Accepteer', 'guardian.reject': 'Weiger', 'guardian.complete': 'Voltooien', 'guardian.awaiting_others': 'wacht op de andere partijen', 'guardian.coguard': 'mede-voogdij-aanvraag', '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_guard_cog_t': 'Mede-voogdij gevraagd', 'push.n_guard_cog_b': 'Er is een guardian-aanvraag voor {who}', '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.', 'msg.guardians_label': 'Jouw guardians', 'msg.waved_at_you': 'zwaaide naar je', 'msg.wave_r1': 'Wat leuk!', 'msg.wave_r2': 'Bel me even', 'msg.wave_back': '👋 Terug', 'msg.wave_sent': 'Zwaai verstuurd.', 'guardian.feed_title': 'Van je wards', 'guardian.feed_sub': 'Meelezen met wat je wards plaatsen. Alleen kijken.', 'guardian.follow_title': 'Volgverzoeken', 'guardian.follow_sub': 'Iemand wil een van je wards volgen. Jij beslist.', 'guardian.wave': '👋 Zwaai', 'guardian.waved': '👋 verstuurd', '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.accept': 'Accepteer', 'guardian.reject': 'Weiger', 'guardian.complete': 'Voltooien', 'guardian.awaiting_others': 'wacht op de andere partijen', 'guardian.coguard': 'mede-voogdij-aanvraag', '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_guard_cog_t': 'Mede-voogdij gevraagd', 'push.n_guard_cog_b': 'Er is een guardian-aanvraag voor {who}', '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.', 'msg.guardians_label': 'Your guardians', 'msg.waved_at_you': 'waved at you', 'msg.wave_r1': 'Lovely!', 'msg.wave_r2': 'Call me', 'msg.wave_back': '👋 Back', 'msg.wave_sent': 'Wave sent.', 'guardian 2.feed_title': 'Your wards', 'guardian2.feed_sub': 'Read along with what your wards post. Watch only.', 'guardian2.follow_title': 'Follow requests', 'guardian2.follow_sub': 'Someone wants to follow one of your wards. You decide.', 'guardian2.wave': '👋 Wave', 'guardian2.waved': '👋 sent', '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.accept': 'Accept', 'guardian.reject': 'Reject', 'guardian.complete': 'Complete', 'guardian.awaiting_others': 'awaiting the other parties', 'guardian.coguard': 'co-guardianship offer', '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_guard_cog_t': 'Co-guardianship asked', 'push.n_guard_cog_b': 'A guardian offer for {who} needs you', '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.', 'msg.guardians_label': 'Your guardians', 'msg.waved_at_you': 'waved at you', 'msg.wave_r1': 'Lovely!', 'msg.wave_r2': 'Call me', 'msg.wave_back': '👋 Back', 'msg.wave_sent': 'Wave sent.', 'guardian.feed_title': 'Your wards', 'guardian.feed_sub': 'Read along with what your wards post. Watch only.', 'guardian.follow_title': 'Follow requests', 'guardian.follow_sub': 'Someone wants to follow one of your wards. You decide.', 'guardian.wave': '👋 Wave', 'guardian.waved': '👋 sent', '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.accept': 'Accept', 'guardian.reject': 'Reject', 'guardian.complete': 'Complete', 'guardian.awaiting_others': 'awaiting the other parties', 'guardian.coguard': 'co-guardianship offer', '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_guard_cog_t': 'Co-guardianship asked', 'push.n_guard_cog_b': 'A guardian offer for {who} needs you', '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.', 'msg.guardians_label': 'Deine Guardians', 'msg.waved_at_you': 'hat dir zugewinkt', 'msg.wave_r1': 'Wie schön!', 'msg.wave_r2': 'Ruf mich an', 'msg.wave_back': '👋 Zurück', 'msg.wave_sent': 'Winken gesendet.', 'guardian 2.feed_title': 'Deine Wards', 'guardian2.feed_sub': 'Lies mit, was deine Wards posten. Nur schauen.', 'guardian2.follow_title': 'Follow-Anfragen', 'guardian2.follow_sub': 'Jemand möchte einem deiner Wards folgen. Du entscheidest.', 'guardian2.wave': '👋 Winken', 'guardian2.waved': '👋 gesendet', '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.accept': 'Annehmen', 'guardian.reject': 'Ablehnen', 'guardian.complete': 'Abschließen', 'guardian.awaiting_others': 'wartet auf die anderen Parteien', 'guardian.coguard': 'Mit-Vormundschaftsangebot', '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_guard_cog_t': 'Mit-Vormundschaft gefragt', 'push.n_guard_cog_b': 'Ein Guardian-Angebot für {who} braucht dich', '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.', 'msg.guardians_label': 'Deine Guardians', 'msg.waved_at_you': 'hat dir zugewinkt', 'msg.wave_r1': 'Wie schön!', 'msg.wave_r2': 'Ruf mich an', 'msg.wave_back': '👋 Zurück', 'msg.wave_sent': 'Winken gesendet.', 'guardian.feed_title': 'Deine Wards', 'guardian.feed_sub': 'Lies mit, was deine Wards posten. Nur schauen.', 'guardian.follow_title': 'Follow-Anfragen', 'guardian.follow_sub': 'Jemand möchte einem deiner Wards folgen. Du entscheidest.', 'guardian.wave': '👋 Winken', 'guardian.waved': '👋 gesendet', '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.accept': 'Annehmen', 'guardian.reject': 'Ablehnen', 'guardian.complete': 'Abschließen', 'guardian.awaiting_others': 'wartet auf die anderen Parteien', 'guardian.coguard': 'Mit-Vormundschaftsangebot', '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_guard_cog_t': 'Mit-Vormundschaft gefragt', 'push.n_guard_cog_b': 'Ein Guardian-Angebot für {who} braucht dich', '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
r787e0d9 rf1c50f9 26 26 <span class="g-me" title="<%= t('guardian.acting_as') %>">@<%= state.site %></span> 27 27 <% } %> 28 </header> 28 <form method="post" action="/guardian/invite" style="display:inline"> 29 <button class="quiet small" type="submit">Invite a guardian</button> 30 </form> 31 </header> 29 32 30 33 <main> 34 <!-- 0. Wards' corner: meelezen met je wards (read-only). --> 35 <section id="feed-section" hidden> 36 <div class="g-sec-head"><h2><%= t('guardian.feed_title') %></h2></div> 37 <p class="g-sec-sub"><%= t('guardian.feed_sub') %></p> 38 <div id="feed-list" class="g-list"></div> 39 </section> 40 41 <!-- 0b. Follow-verzoeken op je wards (FEP-633c §5.3). --> 42 <section id="follow-section" hidden> 43 <div class="g-sec-head"><h2><%= t('guardian.follow_title') %></h2></div> 44 <p class="g-sec-sub"><%= t('guardian.follow_sub') %></p> 45 <div id="follow-list" class="g-list"></div> 46 </section> 47 31 48 <!-- 1. Berichtencentrum: hulpverzoeken. Dat is waar dit voor bestaat. --> 32 49 <section id="help-section">
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)