Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision cb01666b8ba6e2c17dcfd2f48eaf0a16fb172129)
+++ src/config/database.js	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -39,19 +39,19 @@
   ensureColumn('users', 'reset_token', 'TEXT');
   ensureColumn('users', 'reset_token_expires', 'DATETIME');
-  // Google OAuth: koppel een Google-account aan een user (login via Google).
+  // Google OAuth: link a Google account to a user (login via Google).
   ensureColumn('users', 'google_sub', 'TEXT');
-  // Read-only/kijk-account: kan alles bekijken maar geen wijzigingen doen.
+  // Read-only/viewer account: can view everything but make no changes.
   ensureColumn('users', 'readonly', 'INTEGER DEFAULT 0');
-  // Persoonlijke interface-taal (nl|en|de). Null = volg de standaard (site/env/browser).
+  // Personal interface language (nl|en|de). Null = follow the default (site/env/browser).
   ensureColumn('users', 'lang', 'TEXT');
   // Site-level moderation toggle. 'trust' = auto-approve, 'moderate' = pending until reviewed.
   ensureColumn('sites', 'comments_moderation_mode', "TEXT DEFAULT 'moderate'");
-  // Cirkels: mag deze site in cirkels van anderen verschijnen (surfacing opt-out).
+  // Circles: whether this site may appear in other sites' circles (surfacing opt-out).
   ensureColumn('sites', 'allow_circle', 'INTEGER DEFAULT 1');
 
-  // Eén EXPLICIETE primaire/hoofd-site (= de bedrijfs-/labelsite in hub-modus,
-  // de enige site in solo) i.p.v. de fragiele "oudste = hoofd"-conventie die op
-  // 4 plekken gedupliceerd stond. Backfill: markeer de oudste als er nog geen
-  // primaire site is, zodat bestaand gedrag exact behouden blijft.
+  // One EXPLICIT primary/main site (= the company/label site in hub mode,
+  // the only site in solo) instead of the fragile "oldest = main" convention
+  // that was duplicated in 4 places. Backfill: mark the oldest if no primary
+  // site exists yet, so existing behaviour is preserved exactly.
   ensureColumn('sites', 'is_primary', 'INTEGER DEFAULT 0');
   try {
@@ -61,5 +61,5 @@
       if (oldest) db.prepare('UPDATE sites SET is_primary = 1 WHERE id = ?').run(oldest.id);
     }
-  } catch (e) { /* sites-tabel nog leeg/afwezig bij verse init — ensurePrimarySite regelt 't */ }
+  } catch (e) { /* sites table still empty/absent on fresh init — ensurePrimarySite handles it */ }
 
   // v9 audit additions —————————————————————————————————————————
@@ -82,15 +82,15 @@
   // Per-post noindex + type
   ensureColumn('posts', 'noindex', 'INTEGER DEFAULT 0');
-  ensureColumn('posts', 'publish_at', 'DATETIME');         // release-planning (premium #3): geplande go-live
+  ensureColumn('posts', 'publish_at', 'DATETIME');         // release planning (premium #3): scheduled go-live
   ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0');  // fan-only preview (premium #3)
   ensureColumn('posts', 'type',    "TEXT DEFAULT 'post'");  // post | foto | video | audio
 
-  // Statistieken (premium-module) — kale tellers, cookievrij.
-  ensureColumn('posts', 'view_count', 'INTEGER DEFAULT 0');         // weergaven per post
+  // Statistics (premium module) — bare counters, cookie-free.
+  ensureColumn('posts', 'view_count', 'INTEGER DEFAULT 0');         // views per post
   ensureColumn('audio_tracks', 'play_count', 'INTEGER DEFAULT 0');  // plays per track
-  ensureColumn('audio_tracks', 'downloadable', 'INTEGER DEFAULT 0'); // download-voor-email (premium #2)
-  ensureColumn('audio_tracks', 'credit', 'TEXT');   // eigenaar/credit (copyright-houder)
-  ensureColumn('audio_tracks', 'license', 'TEXT');  // licentie (bv. "CC BY 4.0", "Alle rechten voorbehouden")
-  ensureColumn('audio_tracks', 'link_spotify',    'TEXT');  // "open in"-links per track
+  ensureColumn('audio_tracks', 'downloadable', 'INTEGER DEFAULT 0'); // download-for-email (premium #2)
+  ensureColumn('audio_tracks', 'credit', 'TEXT');   // owner/credit (copyright holder)
+  ensureColumn('audio_tracks', 'license', 'TEXT');  // license (e.g. "CC BY 4.0", "All rights reserved")
+  ensureColumn('audio_tracks', 'link_spotify',    'TEXT');  // "open in" links per track
   ensureColumn('audio_tracks', 'link_youtube',    'TEXT');
   ensureColumn('audio_tracks', 'link_soundcloud', 'TEXT');
@@ -123,6 +123,6 @@
   `);
 
-  // Globale app-instellingen (key/value singleton). O.a. de tenancy-modus
-  // (solo = één site, hub = bedrijfssite + /user/). Default = solo.
+  // Global app settings (key/value singleton). Includes the tenancy mode
+  // (solo = one site, hub = company site + /user/). Default = solo.
   db.exec(`
     CREATE TABLE IF NOT EXISTS app_settings (
@@ -134,9 +134,9 @@
   db.prepare("INSERT OR IGNORE INTO app_settings (key, value) VALUES ('tenancy', 'solo')").run();
 
-  // ── Statistieken (premium) — cookievrij ─────────────────────
-  // stat_daily: per dag per site het aantal pageviews (kale teller).
-  // stat_visitor_day: per dag per site een rij per UNIEKE bezoeker-hash
-  //   (sha256 van IP+UA+dag-salt; de salt roteert dagelijks en wordt nooit
-  //   bewaard → geen persistente identifier, geen cookie, geen toestemming nodig).
+  // ── Statistics (premium) — cookie-free ──────────────────────
+  // stat_daily: pageview count per day per site (bare counter).
+  // stat_visitor_day: one row per UNIQUE visitor hash per day per site
+  //   (sha256 of IP+UA+day-salt; the salt rotates daily and is never stored
+  //   → no persistent identifier, no cookie, no consent required).
   db.exec(`
     CREATE TABLE IF NOT EXISTS stat_daily (
@@ -161,6 +161,6 @@
   `);
 
-  // ── Cirkels (federatie) ─────────────────────────────────────
-  // Decentrale, asymmetrische verbindingen tussen solo-instances.
+  // ── Circles (federation) ────────────────────────────────────
+  // Decentralised, asymmetric connections between solo instances.
   db.exec(`
     CREATE TABLE IF NOT EXISTS circle_links (
@@ -200,10 +200,10 @@
   `);
 
-  // Tags van de originele post — getoond in de cirkel (comma-separated string).
+  // Tags from the original post — shown in the circle feed (comma-separated string).
   ensureColumn('remote_posts', 'tags', 'TEXT');
 
-  // Nieuwsbrief / mailinglijst (premium). Abonnees per site; double opt-in als SMTP
-  // er is (status 'pending' tot bevestigd), anders single opt-in ('confirmed').
-  // 'unsub' = uitgeschreven. token = confirm/unsubscribe-sleutel (in de e-maillinks).
+  // Newsletter / mailing list (premium). Subscribers per site; double opt-in when SMTP
+  // is configured (status 'pending' until confirmed), otherwise single opt-in ('confirmed').
+  // 'unsub' = unsubscribed. token = confirm/unsubscribe key (used in email links).
   db.exec(`
     CREATE TABLE IF NOT EXISTS subscribers (
@@ -221,5 +221,5 @@
   `);
 
-  // Verstuurde nieuwsbrieven (historie + aantallen).
+  // Sent newsletters (history + counts).
   db.exec(`
     CREATE TABLE IF NOT EXISTS newsletters (
@@ -233,5 +233,5 @@
   `);
 
-  // Show-agenda (premium #8): tourdata/optredens per site.
+  // Show agenda (premium #8): tour dates / gigs per site.
   db.exec(`
     CREATE TABLE IF NOT EXISTS shows (
@@ -250,8 +250,8 @@
   `);
 
-  // Meldingen: iemand reageert op je reactie / post, of liket je post. Snapshots
-  // van naam/titel zodat de lijst goedkoop te tonen is zonder joins.
-  // NB: bewust 'user_notifications' — sommige oudere DBs hebben nog een stale,
-  // ongebruikte 'notifications'-tabel met een ander schema (geen read-kolom).
+  // Notifications: someone replies to your comment / post, or likes your post. Snapshots
+  // of name/title so the list can be shown cheaply without joins.
+  // NB: deliberately named 'user_notifications' — some older DBs still have a stale,
+  // unused 'notifications' table with a different schema (no read column).
   db.exec(`
     CREATE TABLE IF NOT EXISTS user_notifications (
@@ -270,6 +270,6 @@
   `);
 
-  // Link-in-bio klikstatistiek (premium #6). Per (site, url) een teller; de
-  // link-in-bio-pagina linkt via /links/go/:i dat de klik telt en doorstuurt.
+  // Link-in-bio click statistics (premium #6). One counter per (site, url); the
+  // link-in-bio page links via /links/go/:i which counts the click and redirects.
   db.exec(`
     CREATE TABLE IF NOT EXISTS link_clicks (
@@ -282,7 +282,7 @@
   `);
 
-  // Likes / favorieten: een ingelogde gebruiker kan een post liken. De set van
-  // posts die een gebruiker likte = z'n favorieten (/favorieten-pagina). Eén rij
-  // per (post, user); uniek zodat liken idempotent is.
+  // Likes / favourites: a logged-in user can like a post. The set of
+  // posts a user liked = their favourites (/favorieten page). One row
+  // per (post, user); unique so that liking is idempotent.
   db.exec(`
     CREATE TABLE IF NOT EXISTS post_likes (
Index: src/config/features.js
===================================================================
--- src/config/features.js	(revision cb01666b8ba6e2c17dcfd2f48eaf0a16fb172129)
+++ src/config/features.js	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -1,11 +1,11 @@
-// Feature-flags (boot-tijd, via env).
+// Feature flags (boot-time, via env).
 //
-// Lite-modus: zet KLONKT_AUDIO=off in .env om de HELE audio-feature uit te
-// schakelen — geen audio-/playlist-/download-/embed-routes, geen ffmpeg-aanroep,
-// geen speler en geen [[track]]/[[playlist]]-shortcodes. Zo draait Klonkt als
-// lichte blog/foto/EPK-site op een omgeving zónder ffmpeg/exec. Hub én Cirkels
-// blijven gewoon werken (die hangen niet van audio af).
+// Lite mode: set KLONKT_AUDIO=off in .env to disable the ENTIRE audio feature —
+// no audio/playlist/download/embed routes, no ffmpeg calls, no player, and no
+// [[track]]/[[playlist]] shortcodes. This lets Klonkt run as a lightweight
+// blog/photo/EPK site on environments without ffmpeg/exec. Hub and Circles
+// keep working (they have no audio dependency).
 //
-// Default = aan (volledige versie). Alleen de letterlijke waarde 'off' schakelt uit.
+// Default = on (full version). Only the literal value 'off' disables it.
 export function audioEnabled() {
   return String(process.env.KLONKT_AUDIO ?? 'on').toLowerCase() !== 'off';
Index: src/config/google.js
===================================================================
--- src/config/google.js	(revision cb01666b8ba6e2c17dcfd2f48eaf0a16fb172129)
+++ src/config/google.js	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -1,12 +1,12 @@
-// Google OAuth2 voor LUISTERAARS (reageren). Per-instance: de self-hoster zet
-// z'n EIGEN Google-client. Zo hangt elke site aan z'n eigen Google Cloud project
-// — geen centrale afhankelijkheid, geen gedeelde aansprakelijkheid.
+// Google OAuth2 for LISTENERS (commenting). Per-instance: each self-hoster sets
+// their OWN Google client. This way every site is tied to its own Google Cloud
+// project — no central dependency, no shared liability.
 //
-// Config-bron (in deze volgorde): app_settings (ingesteld via Beheer → Instellingen),
-// anders de env-vars. Niet ingesteld → geen "Login met Google"-knop; de rest van
-// de site werkt door. Google-login geeft NOOIT beheerrechten.
+// Config source (in this order): app_settings (set via Admin → Settings),
+// otherwise env vars. Not configured → no "Login with Google" button; the rest
+// of the site keeps working. Google login NEVER grants admin rights.
 //
-// De redirect-URI wordt afgeleid van PUBLIC_BASE_URL (<base>/auth/google/callback),
-// of expliciet via GOOGLE_REDIRECT_URI. Die exacte URL moet in Google Cloud staan.
+// The redirect URI is derived from PUBLIC_BASE_URL (<base>/auth/google/callback),
+// or explicitly via GOOGLE_REDIRECT_URI. That exact URL must be listed in Google Cloud.
 
 import { getSetting } from '../services/SettingsService.js';
@@ -16,5 +16,5 @@
 const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
 
-// Dynamisch lezen (UI-wijziging werkt zonder herstart). app_settings wint, env = fallback.
+// Read dynamically (UI changes take effect without a restart). app_settings wins, env = fallback.
 function clientId() {
   return getSetting('google_client_id', '') || process.env.GOOGLE_CLIENT_ID || '';
@@ -29,5 +29,5 @@
 }
 
-export function currentClientId() { return clientId(); } // niet-geheim, voor het formulier
+export function currentClientId() { return clientId(); } // not secret, used for the settings form
 export function clientSecretSet() { return !!clientSecret(); }
 export function googleConfigured() {
@@ -61,5 +61,5 @@
     body,
   });
-  if (!r.ok) throw new Error(`Google token-exchange faalde: ${r.status}`);
+  if (!r.ok) throw new Error(`Google token exchange failed: ${r.status}`);
   return r.json(); // { access_token, id_token, ... }
 }
@@ -68,5 +68,5 @@
 export async function fetchUserinfo(accessToken) {
   const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
-  if (!r.ok) throw new Error(`Google userinfo faalde: ${r.status}`);
+  if (!r.ok) throw new Error(`Google userinfo failed: ${r.status}`);
   return r.json();
 }
Index: src/config/mailer.js
===================================================================
--- src/config/mailer.js	(revision cb01666b8ba6e2c17dcfd2f48eaf0a16fb172129)
+++ src/config/mailer.js	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -1,8 +1,8 @@
-// E-mail versturen (wachtwoord-reset, nieuwsbrief, notify). Optioneel: alleen actief
-// als SMTP is ingesteld — via Beheer → Instellingen (app_settings) OF env-vars.
+// Send email (password reset, newsletter, notify). Optional: only active
+// when SMTP is configured — via Admin → Settings (app_settings) OR env vars.
 //
-// Config-bron (in deze volgorde): app_settings (ingesteld in de UI), anders env:
+// Config source (in this order): app_settings (set via the UI), otherwise env:
 //   SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS, SMTP_FROM (default = USER)
-// Niet ingesteld → versturen valt terug op CLI (reset-admin) / wordt overgeslagen.
+// Not configured → sending falls back to CLI (reset-admin) / is skipped.
 
 import nodemailer from 'nodemailer';
@@ -23,5 +23,5 @@
 }
 
-// Status voor de UI (zonder het wachtwoord te lekken).
+// Status for the UI (without leaking the password).
 export function mailerStatus() {
   const c = cfg();
@@ -33,10 +33,10 @@
     from: c.from,
     passSet: !!c.pass,
-    // bron: handig om te tonen dat env nog actief is
+    // source: useful to show that env vars are still active
     fromEnv: !getSetting('smtp_host', '') && !!process.env.SMTP_HOST,
   };
 }
 
-// Transport cachen, maar herbouwen zodra de config wijzigt (UI-edit zonder herstart).
+// Cache the transport, but rebuild it whenever the config changes (UI edit without restart).
 let _transport = null, _key = null;
 function transport() {
@@ -47,5 +47,5 @@
       host: c.host,
       port: c.port,
-      secure: c.port === 465, // 465 = impliciete TLS; 587 = STARTTLS
+      secure: c.port === 465, // 465 = implicit TLS; 587 = STARTTLS
       auth: { user: c.user, pass: c.pass },
     });
@@ -56,5 +56,5 @@
 
 export async function sendMail({ to, subject, text, html }) {
-  if (!mailerConfigured()) throw new Error('SMTP niet geconfigureerd');
+  if (!mailerConfigured()) throw new Error('SMTP not configured');
   const c = cfg();
   return transport().sendMail({ from: c.from, to, subject, text, html });
