Index: src/views/pages/admin-paid.ejs
===================================================================
--- src/views/pages/admin-paid.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/admin-paid.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -1,76 +1,73 @@
 <div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
-  <h1 style="margin:0 0 .3rem">Betaalde posts</h1>
+  <h1 style="margin:0 0 .3rem"><%= t('apaid.t') %></h1>
   <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
-    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.
+    <%= t('apaid.intro') %>
   </p>
 
-  <% if (saved) { %><p style="color:var(--accent);font-weight:600">Opgeslagen.</p><% } %>
+  <% if (saved) { %><p style="color:var(--accent);font-weight:600"><%= t('apaid.saved') %></p><% } %>
   <% if (error) { %><p style="color:#c0392b"><%= error %></p><% } %>
   <% if (!secretReady) { %>
-    <p style="color:#c0392b">Let op: de encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?). Zonder sleutel kunnen secrets niet veilig worden opgeslagen.</p>
+    <p style="color:#c0392b"><%= t('apaid.nokey') %></p>
   <% } %>
 
   <p style="margin:.2rem 0 1.2rem">
-    Status:
+    <%= t('apaid.status') %>
     <% if (status.connected) { %>
-      <strong style="color:var(--accent)">verbonden</strong> (campagne <%= status.campaignId %>)
+      <strong style="color:var(--accent)"><%= t('apaid.connected') %></strong> (<%= t('apaid.campaign') %> <%= status.campaignId %>)
     <% } else if (status.configured) { %>
-      <strong>ingesteld, nog niet verbonden</strong> (vul een token in)
+      <strong><%= t('apaid.configured') %></strong>
     <% } else { %>
-      <strong>nog niet ingesteld</strong>
+      <strong><%= t('apaid.notyet') %></strong>
     <% } %>
   </p>
 
   <div style="border:1px solid var(--border,#333);border-radius:10px;padding:.9rem 1rem;margin:0 0 1.3rem;background:color-mix(in srgb,var(--accent,#6b8f71) 8%,transparent)">
-    <p style="margin:0 0 .4rem;font-weight:600">Zet deze redirect-URI in je Patreon-client</p>
+    <p style="margin:0 0 .4rem;font-weight:600"><%= t('apaid.redirect_h') %></p>
     <p style="margin:0 0 .6rem;color:var(--ink-soft,#888);font-size:.9rem">
-      Bij je Patreon API-client, onder <em>Redirect URIs</em>, moet exact deze regel staan.
-      Klopt hij niet, dan geeft Patreon een foutmelding in plaats van je supporters terug te sturen.
+      <%= t('apaid.redirect_p') %>
     </p>
     <div style="display:flex;gap:.5rem;align-items:center">
       <input id="pd-redirect" type="text" readonly value="<%= redirectUri %>" style="flex:1;min-width:0;font-family:monospace;font-size:.9rem">
-      <button type="button" id="pd-copy" class="btn">Kopieer</button>
+      <button type="button" id="pd-copy" class="btn" data-copied="<%= t('apaid.copied') %>"><%= t('apaid.copy') %></button>
     </div>
   </div>
 
   <form method="post" action="/admin/paid" style="display:flex;flex-direction:column;gap:.9rem">
-    <label>Patreon client id
+    <label><%= t('apaid.client_id') %>
       <input type="text" name="client_id" value="<%= status.clientId || '' %>" autocomplete="off" style="width:100%">
     </label>
-    <label>Patreon client secret
-      <input type="password" name="client_secret" placeholder="<%= status.hasSecret ? 'blijft ongewijzigd' : '' %>" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)">Leeg laten = huidige waarde behouden.</small>
+    <label><%= t('apaid.client_secret') %>
+      <input type="password" name="client_secret" placeholder="<%= status.hasSecret ? t('apaid.unchanged') : '' %>" autocomplete="off" style="width:100%">
+      <small style="color:var(--ink-soft,#888)"><%= t('apaid.keep') %></small>
     </label>
-    <label>Campagne-id
+    <label><%= t('apaid.campaign_id') %>
       <input type="text" name="campaign_id" value="<%= status.campaignId || '' %>" autocomplete="off" style="width:100%">
     </label>
-    <label>Openbare Patreon-pagina
+    <label><%= t('apaid.public_page') %>
       <input type="url" name="patreon_url" value="<%= status.patreonUrl || '' %>" placeholder="https://www.patreon.com/jouwnaam" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)">De link waar bezoekers supporter kunnen worden. Getoond als "Word supporter" wanneer iemand nog niet doneert.</small>
+      <small style="color:var(--ink-soft,#888)"><%= t('apaid.public_help') %></small>
     </label>
-    <label>Creator access token
-      <input type="password" name="access_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
+    <label><%= t('apaid.access') %>
+      <input type="password" name="access_token" placeholder="<%= t('apaid.unchanged') %>" autocomplete="off" style="width:100%">
     </label>
-    <label>Creator refresh token
-      <input type="password" name="refresh_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)">De access + refresh token krijg je op je Patreon API-clientpagina. Wij versleutelen ze en verversen automatisch.</small>
+    <label><%= t('apaid.refresh') %>
+      <input type="password" name="refresh_token" placeholder="<%= t('apaid.unchanged') %>" autocomplete="off" style="width:100%">
+      <small style="color:var(--ink-soft,#888)"><%= t('apaid.token_help') %></small>
     </label>
-    <label>Standaard-steunbedrag voor een betaalde post (euro)
+    <label><%= t('apaid.min_eur') %>
       <input type="text" name="default_min_eur" value="<%= status.defaultMinCents ? (status.defaultMinCents/100).toFixed(2) : '' %>" inputmode="decimal" style="width:120px">
     </label>
     <div style="display:flex;gap:.6rem;align-items:center">
-      <button type="submit" class="btn btn-primary">Opslaan</button>
+      <button type="submit" class="btn btn-primary"><%= t('apaid.save') %></button>
     </div>
   </form>
 
   <% if (status.configured || status.connected) { %>
-    <form method="post" action="/admin/paid/disconnect" data-confirm="Patreon-koppeling verwijderen?" style="margin-top:1rem">
-      <button type="submit" class="btn btn-danger">Koppeling verwijderen</button>
+    <form method="post" action="/admin/paid/disconnect" data-confirm="<%= t('apaid.disconnect_confirm') %>" style="margin-top:1rem">
+      <button type="submit" class="btn btn-danger"><%= t('apaid.disconnect') %></button>
     </form>
   <% } %>
 
-  <p style="margin-top:1.5rem"><a href="/admin">&larr; Terug naar Beheer</a></p>
+  <p style="margin-top:1.5rem"><a href="/admin">&larr; <%= t('admin.back') %></a></p>
 </div>
 
@@ -83,5 +80,5 @@
   btn.addEventListener('click', function () {
     field.select();
-    var done = function () { var t = btn.textContent; btn.textContent = 'Gekopieerd'; setTimeout(function () { btn.textContent = t; }, 1400); };
+    var done = function () { var t = btn.textContent; btn.textContent = document.getElementById('pd-copy').getAttribute('data-copied'); setTimeout(function () { btn.textContent = t; }, 1400); };
     if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(field.value).then(done, function () { try { document.execCommand('copy'); done(); } catch (e) {} }); }
     else { try { document.execCommand('copy'); done(); } catch (e) {} }
Index: src/views/pages/admin-push.ejs
===================================================================
--- src/views/pages/admin-push.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/admin-push.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -1,47 +1,44 @@
 <div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
-  <h1 style="margin:0 0 .3rem">Notificaties</h1>
+  <h1 style="margin:0 0 .3rem"><%= t('push.t') %></h1>
   <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
-    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.
+    <%= t('push.intro') %>
   </p>
 
   <% if (!vapidKey) { %>
-    <p style="color:#c0392b">Push is op deze server niet beschikbaar (sleutel kon niet worden aangemaakt of de dependency ontbreekt).</p>
+    <p style="color:#c0392b"><%= t('push.unavailable') %></p>
   <% } else { %>
 
   <div id="np-unsupported" hidden style="color:#c0392b;margin:0 0 1rem">
-    Deze browser ondersteunt geen push-notificaties.
+    <%= t('push.unsupported') %>
   </div>
   <div id="np-ios-hint" hidden style="border:1px solid var(--border,#333);border-radius:10px;padding:.8rem 1rem;margin:0 0 1rem;font-size:.92rem;color:var(--ink-soft,#888)">
-    Op iPhone/iPad werkt dit alleen als de site op je beginscherm staat:
-    deel-knop → <em>Zet op beginscherm</em>, en open 'm daarna vanaf daar.
+    <%= t('push.ios_hint') %>
   </div>
 
   <section style="border:1px solid var(--border,#333);border-radius:12px;padding:1rem 1.1rem;margin:0 0 1.2rem">
-    <p style="margin:0 0 .6rem"><strong>Dit apparaat:</strong> <span id="np-state">controleren…</span></p>
+    <p style="margin:0 0 .6rem"><strong><%= t('push.this_device') %></strong> <span id="np-state"><%= t('push.checking') %></span></p>
     <div style="display:flex;gap:.6rem;flex-wrap:wrap">
-      <button type="button" id="np-on" class="btn btn-primary" hidden>Zet aan op dit apparaat</button>
-      <button type="button" id="np-off" class="btn" hidden>Zet uit</button>
-      <button type="button" id="np-test" class="btn" hidden>Stuur testmelding</button>
+      <button type="button" id="np-on" class="btn btn-primary" hidden><%= t('push.enable') %></button>
+      <button type="button" id="np-off" class="btn" hidden><%= t('push.disable') %></button>
+      <button type="button" id="np-test" class="btn" hidden><%= t('push.test') %></button>
     </div>
     <fieldset id="np-alerts" hidden style="border:none;padding:0;margin:1rem 0 0">
-      <legend style="font-weight:600;margin-bottom:.4rem">Waarvoor wil je een melding?</legend>
-      <label style="display:block"><input type="checkbox" data-alert="follow"> Nieuwe volger</label>
-      <label style="display:block"><input type="checkbox" data-alert="reply"> Reactie of vermelding</label>
-      <label style="display:block"><input type="checkbox" data-alert="like"> Waardering (ster)</label>
-      <label style="display:block"><input type="checkbox" data-alert="boost"> Boost</label>
-      <label style="display:block"><input type="checkbox" data-alert="dm"> Priv&eacute;bericht</label>
-      <p id="np-saved" hidden style="color:var(--accent);margin:.4rem 0 0">Opgeslagen.</p>
+      <legend style="font-weight:600;margin-bottom:.4rem"><%= t('push.what') %></legend>
+      <label style="display:block"><input type="checkbox" data-alert="follow"> <%= t('push.a_follow') %></label>
+      <label style="display:block"><input type="checkbox" data-alert="reply"> <%= t('push.a_reply') %></label>
+      <label style="display:block"><input type="checkbox" data-alert="like"> <%= t('push.a_like') %></label>
+      <label style="display:block"><input type="checkbox" data-alert="boost"> <%= t('push.a_boost') %></label>
+      <label style="display:block"><input type="checkbox" data-alert="dm"> <%= t('push.a_dm') %></label>
+      <p id="np-saved" hidden style="color:var(--accent);margin:.4rem 0 0"><%= t('push.saved') %></p>
     </fieldset>
   </section>
 
   <% if (subscriptions.length) { %>
-    <h2 style="font-size:1.05rem;margin:0 0 .5rem">Gekoppelde apparaten</h2>
+    <h2 style="font-size:1.05rem;margin:0 0 .5rem"><%= t('push.devices') %></h2>
     <ul style="list-style:none;padding:0;margin:0 0 1.2rem">
       <% subscriptions.forEach(function (s) { %>
         <li data-endpoint="<%= s.endpoint %>" style="display:flex;justify-content:space-between;gap:.8rem;align-items:center;padding:.45rem 0;border-bottom:1px solid var(--border,#2a2a2a)">
-          <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><%= s.ua_label || 'Apparaat' %> <small style="color:var(--ink-soft,#888)">sinds <%= String(s.created_at || '').slice(0, 10) %></small></span>
-          <button type="button" class="btn np-remove" data-endpoint="<%= s.endpoint %>">Verwijder</button>
+          <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><%= s.ua_label || t('push.device') %> <small style="color:var(--ink-soft,#888)"><%= t('push.since') %> <%= String(s.created_at || '').slice(0, 10) %></small></span>
+          <button type="button" class="btn np-remove" data-endpoint="<%= s.endpoint %>"><%= t('push.remove') %></button>
         </li>
       <% }); %>
@@ -51,5 +48,5 @@
   <% } %>
 
-  <p style="margin-top:1.5rem"><a href="/admin">&larr; Terug naar Beheer</a></p>
+  <p style="margin-top:1.5rem"><a href="/admin">&larr; <%= t('admin.back') %></a></p>
 </div>
 
@@ -59,4 +56,5 @@
   alerts: defaultAlerts,
   saved: subscriptions.reduce(function (m, s) { try { m[s.endpoint] = JSON.parse(s.alert_types || '{}'); } catch (e) { m[s.endpoint] = {}; } return m; }, {}),
+  i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') },
 }).replace(/</g, '\\u003c') %></script>
 <script>
@@ -74,5 +72,5 @@
   if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) {
     document.getElementById('np-unsupported').hidden = false;
-    elState.textContent = 'niet ondersteund';
+    elState.textContent = cfg.i18n.unsupported;
     return;
   }
@@ -107,5 +105,5 @@
   function render(sub) {
     currentEndpoint = sub ? sub.endpoint : null;
-    elState.textContent = sub ? 'meldingen staan aan' : (Notification.permission === 'denied' ? 'geblokkeerd in de browserinstellingen' : 'meldingen staan uit');
+    elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off);
     btnOn.hidden = !!sub || Notification.permission === 'denied';
     btnOff.hidden = !sub;
@@ -120,5 +118,5 @@
     setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts);
     render(sub);
-  }).catch(function () { elState.textContent = 'status onbekend'; });
+  }).catch(function () { elState.textContent = cfg.i18n.unknown; });
 
   btnOn.addEventListener('click', function () {
@@ -131,5 +129,5 @@
         return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() })
           .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); });
-      }).catch(function () { btnOn.disabled = false; elState.textContent = 'aanzetten mislukt'; });
+      }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; });
     });
   });
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/admin.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -19,4 +19,6 @@
   <% if (tenancy === 'hub') { %>
     <p class="admin-tagline"><%= t('admin.tagline_hub') %></p>
+  <% } else if (typeof circlesOn !== 'undefined' && circlesOn) { %>
+    <p class="admin-tagline"><%= t('admin.tagline_cirkels') %></p>
   <% } else { %>
     <p class="admin-tagline"><%= t('admin.tagline_solo') %></p>
@@ -46,5 +48,5 @@
     <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
       <a href="/admin/stats" class="btn"><%= t('admin.b_stats') %></a>
-      <a href="/admin/paid" class="btn">Betaalde posts</a>
+      <a href="/admin/paid" class="btn"><%= t('admin.b_paid') %></a>
       <a href="/admin/newsletter" class="btn"><%= t('admin.b_newsletter') %></a>
       <% if (tenancy !== 'hub' && primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
@@ -55,5 +57,5 @@
     <%# Fediverse pages (News / Notifications / My replies / Blocking) are reached via the
         "Fediverse" nav button + the bell + the tabbed section — not duplicated here. %>
-    <a href="/admin/push" class="btn">Notificaties</a>
+    <a href="/admin/push" class="btn"><%= t('admin.b_push') %></a>
     <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
     <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
Index: src/views/pages/fedi-notifications.ejs
===================================================================
--- src/views/pages/fedi-notifications.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/fedi-notifications.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -34,5 +34,13 @@
               <% if (n.type === 'mention' && n.note_url) { %><a class="nt-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
             </div>
-            <% if (n.type === 'report' && n.content) { %><div class="nt-content"><%= n.content %></div>
+            <% if (n.type === 'report') { %>
+              <% if (n.content) { %><div class="nt-content"><%= n.content %></div><% } else { %><div class="nt-content nt-noreason"><%= t('notif.report_noreason') %></div><% } %>
+              <% if (n.objects && n.objects.length) { %>
+                <div class="nt-report-about">
+                  <% n.objects.forEach(function (o) { %>
+                    <a class="nt-post" href="/<%= o.slug %>"><%= t('notif.report_about') %>: <%= o.title %></a>
+                  <% }); %>
+                </div>
+              <% } %>
             <% } else if ((n.type === 'reply' || n.type === 'mention') && n.content) { %><div class="nt-content"><%- n.content %></div><% } %>
           </div>
@@ -58,4 +66,6 @@
     display: inline-flex; align-items: center; justify-content: center; }
   .nt-icon svg { width: 18px; height: 18px; }
+  .nt-report-about { margin-top: .35rem; display: flex; flex-direction: column; gap: .15rem; }
+  .nt-noreason { font-style: italic; opacity: .7; }
   .nt-like .nt-icon { background: color-mix(in srgb, #e8b04b 16%, transparent); color: #e8b04b; }
   .nt-boost .nt-icon { background: color-mix(in srgb, #2fa85a 16%, transparent); color: #2fa85a; }
Index: src/views/pages/paid-gate.ejs
===================================================================
--- src/views/pages/paid-gate.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/paid-gate.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -13,17 +13,15 @@
   <section class="pg-card">
     <div class="pg-lock">💶</div>
-    <h2 class="pg-h2">Voor supporters</h2>
+    <h2 class="pg-h2"><%= t('pgate.h') %></h2>
     <p class="pg-sub">
-      Deze post is voor supporters van deze site
-      <% if (typeof pgCents !== 'undefined' && pgCents) { %>(vanaf &euro;<%= (pgCents/100).toFixed(2) %> per maand op Patreon)<% } %>.
-      Word supporter en ontgrendel 'm daarna met een passkey. Geen account op deze site, geen cookie.
+      <% if (typeof pgCents !== 'undefined' && pgCents) { %><%= t('pgate.sub_cents', { eur: (pgCents/100).toFixed(2) }) %><% } else { %><%= t('pgate.sub') %><% } %>
     </p>
 
     <div class="pg-actions">
       <% if (_hasPatron) { %>
-        <a class="pg-btn" href="<%= pgPatronUrl %>" target="_blank" rel="noopener">Word supporter op Patreon</a>
-        <button type="button" id="pg-unlock" class="pg-btn pg-btn-ghost">Al supporter? Ontgrendelen</button>
+        <a class="pg-btn" href="<%= pgPatronUrl %>" target="_blank" rel="noopener"><%= t('pgate.join') %></a>
+        <button type="button" id="pg-unlock" class="pg-btn pg-btn-ghost"><%= t('pgate.unlock_have') %></button>
       <% } else { %>
-        <button type="button" id="pg-unlock" class="pg-btn">Ontgrendelen met Patreon</button>
+        <button type="button" id="pg-unlock" class="pg-btn"><%= t('pgate.unlock') %></button>
       <% } %>
     </div>
@@ -38,4 +36,5 @@
   var slug = "<%= pgSlug %>";
   var hasPatron = <%= _hasPatron ? 'true' : 'false' %>;
+  var I = { join: "<%= t('pgate.join_short') %>", confirm: "<%= t('pgate.confirm') %>", failed: "<%= t('pgate.failed') %>", error: "<%= t('pgate.error') %>" };
   var btn = document.getElementById('pg-unlock');
   var status = document.getElementById('pg-status');
@@ -48,5 +47,5 @@
   if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
     if (hasPatron) { btn.style.display = 'none'; }
-    else { btn.textContent = 'Word supporter'; btn.addEventListener('click', toLink); }
+    else { btn.textContent = I.join; btn.addEventListener('click', toLink); }
     return;
   }
@@ -54,5 +53,5 @@
   btn.addEventListener('click', function () {
     btn.disabled = true;
-    say('Bevestig met je passkey…');
+    say(I.confirm);
     fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
       .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
@@ -75,5 +74,5 @@
           toLink();   // no valid passkey yet (or lapsed tier): link via Patreon
         } else {
-          btn.disabled = false; say('Ontgrendelen mislukt. Probeer opnieuw.', true);
+          btn.disabled = false; say(I.failed, true);
         }
       })
@@ -81,5 +80,5 @@
         if (e && e.link) { toLink(); return; }
         if (e && e.name === 'NotAllowedError') { toLink(); return; }   // cancelled / no passkey -> link
-        btn.disabled = false; say('Er ging iets mis. Probeer opnieuw.', true);
+        btn.disabled = false; say(I.error, true);
       });
   });
Index: src/views/pages/paid-passkey.ejs
===================================================================
--- src/views/pages/paid-passkey.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/paid-passkey.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -2,10 +2,9 @@
   <div class="pk-card">
     <div class="pk-ic">🔑</div>
-    <h1 class="pk-h1">Je bent supporter, mooi.</h1>
+    <h1 class="pk-h1"><%= t('ppk.h') %></h1>
     <p class="pk-sub">
-      Maak nu een passkey aan. Die wordt je sleutel voor betaalde posts, zonder
-      account en zonder cookie. We bewaren geen naam of e-mailadres.
+      <%= t('ppk.sub') %>
     </p>
-    <button type="button" id="pk-go" class="pk-btn">Maak passkey</button>
+    <button type="button" id="pk-go" class="pk-btn"><%= t('ppk.make') %></button>
     <p id="pk-status" class="pk-status" hidden></p>
   </div>
@@ -17,4 +16,5 @@
   var options = <%- optionsJson %>;
   var blob = "<%= regBlob %>";
+  var I = { unsupported: "<%= t('ppk.unsupported') %>", follow: "<%= t('ppk.follow') %>", done: "<%= t('ppk.done') %>", failed: "<%= t('ppk.failed') %>", cancelled: "<%= t('ppk.cancelled') %>", error: "<%= t('pgate.error') %>" };
   var postUrl = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>/<%= postSlug %>";
   var btn = document.getElementById('pk-go');
@@ -24,10 +24,10 @@
   if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
     btn.disabled = true;
-    say('Passkeys worden niet ondersteund in deze browser.', true);
+    say(I.unsupported, true);
     return;
   }
   btn.addEventListener('click', function () {
     btn.disabled = true;
-    say('Volg de vraag van je apparaat…');
+    say(I.follow);
     window.SimpleWebAuthnBrowser.startRegistration({ optionsJSON: options })
       .then(function (response) {
@@ -39,8 +39,8 @@
       .then(function (r) { return r.json(); })
       .then(function (j) {
-        if (j && j.ok) { say('Gelukt. Je passkey is aangemaakt.'); setTimeout(function () { location.href = postUrl; }, 900); }
-        else { btn.disabled = false; say('Aanmaken mislukt (' + ((j && j.error) || 'onbekend') + '). Probeer opnieuw.', true); }
+        if (j && j.ok) { say(I.done); setTimeout(function () { location.href = postUrl; }, 900); }
+        else { btn.disabled = false; say(I.failed.replace('{err}', (j && j.error) || '?'), true); }
       })
-      .catch(function (e) { btn.disabled = false; say(e && e.name === 'NotAllowedError' ? 'Geannuleerd.' : 'Er ging iets mis. Probeer opnieuw.', true); });
+      .catch(function (e) { btn.disabled = false; say(e && e.name === 'NotAllowedError' ? I.cancelled : I.error, true); });
   });
 })();
Index: src/views/pages/paid-result.ejs
===================================================================
--- src/views/pages/paid-result.ejs	(revision 053bf51d2047068ccfdc7ac5a76e3c8275bba1b5)
+++ src/views/pages/paid-result.ejs	(revision 9a00f283dea4b5647c585540cdef7d52ced93298)
@@ -8,28 +8,25 @@
     <div class="pr-ic">🔒</div>
     <% if (reason === 'notpatron') { %>
-      <h1 class="pr-h1">Nog geen supporter</h1>
-      <p class="pr-sub">Je bent (nog) geen actieve supporter van deze site op Patreon. Word supporter en probeer het daarna opnieuw vanaf de post.</p>
+      <h1 class="pr-h1"><%= t('pres.notpatron_h') %></h1>
+      <p class="pr-sub"><%= t('pres.notpatron_p') %></p>
     <% } else if (reason === 'tier') { %>
-      <h1 class="pr-h1">Een niveau hoger nodig</h1>
-      <p class="pr-sub">
-        Deze post vraagt vanaf &euro;<%= ((typeof neededCents !== 'undefined' ? neededCents : 0)/100).toFixed(2) %>.
-        Jouw steun is nu &euro;<%= ((typeof haveCents !== 'undefined' ? haveCents : 0)/100).toFixed(2) %>. Verhoog je steun en probeer opnieuw.
-      </p>
+      <h1 class="pr-h1"><%= t('pres.tier_h') %></h1>
+      <p class="pr-sub"><%= t('pres.tier_p', { need: ((typeof neededCents !== 'undefined' ? neededCents : 0)/100).toFixed(2), have: ((typeof haveCents !== 'undefined' ? haveCents : 0)/100).toFixed(2) }) %></p>
     <% } else if (reason === 'expired') { %>
-      <h1 class="pr-h1">Aanvraag verlopen</h1>
-      <p class="pr-sub">Deze ontgrendel-link is verlopen of al gebruikt. Ga terug naar de post en probeer het opnieuw.</p>
+      <h1 class="pr-h1"><%= t('pres.expired_h') %></h1>
+      <p class="pr-sub"><%= t('pres.expired_p') %></p>
     <% } else { %>
-      <h1 class="pr-h1">Ontgrendelen afgebroken</h1>
-      <p class="pr-sub">Er is niets gekoppeld. Je kunt het opnieuw proberen vanaf de post.</p>
+      <h1 class="pr-h1"><%= t('pres.declined_h') %></h1>
+      <p class="pr-sub"><%= t('pres.declined_p') %></p>
     <% } %>
 
     <div class="pr-actions">
       <% if ((reason === 'notpatron' || reason === 'tier') && _patron) { %>
-        <a class="pr-btn" href="<%= _patron %>" target="_blank" rel="noopener">Word supporter op Patreon</a>
+        <a class="pr-btn" href="<%= _patron %>" target="_blank" rel="noopener"><%= t('pres.join') %></a>
       <% } %>
       <% if (_slug) { %>
-        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/<%= _slug %>">Terug naar de post</a>
+        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/<%= _slug %>"><%= t('pres.back_post') %></a>
       <% } else { %>
-        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/">Terug naar de site</a>
+        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/"><%= t('pres.back_site') %></a>
       <% } %>
     </div>
