Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/middleware/render.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -20,4 +20,5 @@
 import { isPremium as isPremiumInstance, premiumEnabled, premiumUnlocked } from '../services/PatreonService.js';
 import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js';
+import { t as i18nT, resolveLang, SUPPORTED as LANGS, LANG_NAMES } from '../services/i18n.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -77,7 +78,13 @@
   const canSeeBeheer = !!(_u && (_role === 'god' || _role === 'admin' || _role === 'kijker' || userOwnsSite));
 
+  // Interface-taal (bezoeker-instelbaar): sessie-keuze → browser-taal → nl.
+  const _lang = resolveLang(req);
+
   // Common locals
   const locals = {
     user: _u,
+    lang: _lang,
+    t: (key, vars) => i18nT(_lang, key, vars),
+    langs: LANGS.map((c) => ({ code: c, name: LANG_NAMES[c], active: c === _lang })),
     userOwnsSite,
     canSeeBeheer,
Index: src/routes/lang.js
===================================================================
--- src/routes/lang.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
+++ src/routes/lang.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -0,0 +1,22 @@
+// Taalkeuze van de bezoeker: /lang/:code zet de interface-taal in de sessie en
+// stuurt terug naar waar je vandaan kwam. (Content blijft in de taal van de auteur.)
+import express from 'express';
+import { SUPPORTED } from '../services/i18n.js';
+
+const router = express.Router();
+
+router.get('/lang/:code', (req, res) => {
+  const code = SUPPORTED.includes(req.params.code) ? req.params.code : 'nl';
+  if (req.session) req.session.lang = code;
+  // Veilige terug-URL: alleen een intern pad (geen open redirect).
+  let back = (typeof req.query.r === 'string') ? req.query.r : '';
+  if (!back.startsWith('/') || back.startsWith('//')) {
+    try {
+      const u = new URL(req.get('referer') || '');
+      back = u.pathname + (u.search || '');
+    } catch { back = '/'; }
+  }
+  res.redirect(back || '/');
+});
+
+export default router;
Index: src/server.js
===================================================================
--- src/server.js	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/server.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -46,4 +46,5 @@
 import artistsRoutes from './routes/artists.js';
 import postsRoutes from './routes/posts.js';
+import langRoutes from './routes/lang.js';
 import federationRoutes from './routes/federation.js';
 import { startCircleSyncLoop } from './services/CircleService.js';
@@ -288,4 +289,5 @@
 app.use('/', showsRoutes); // /shows agenda + notify-me (premium)
 app.use('/', changelogRoutes); // /changelog publieke release-/wijzigingen-pagina
+app.use('/', langRoutes); // /lang/:code — interface-taal kiezen (vóór de catch-all)
 app.use('/', postsRoutes);
 
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
+++ src/services/i18n.js	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -0,0 +1,117 @@
+// i18n — eenvoudige interface-vertaling (UI-strings), bezoeker-instelbaar.
+//
+// Taalkeuze: req.session.lang (gezet via /lang/:code) → anders browser-taal
+// (Accept-Language) → anders 'nl'. De helper t(lang, key, vars) zoekt de string op
+// in DICT[lang], valt terug op 'nl', dan op de key zelf. Alleen INTERFACE-teksten;
+// door gebruikers geschreven content (posts, sitenaam, bio) wordt niet vertaald.
+
+export const SUPPORTED = ['nl', 'en', 'de'];
+export const LANG_NAMES = { nl: 'Nederlands', en: 'English', de: 'Deutsch' };
+
+const DICT = {
+  nl: {
+    'nav.back_to_site': '← Terug naar site',
+    'nav.home': 'Home',
+    'nav.archive': 'Archief',
+    'nav.search': 'Zoeken',
+    'nav.theme': 'Thema wisselen',
+    'nav.install': 'App installeren',
+    'nav.login': 'Inloggen',
+    'nav.logout': 'Uitloggen',
+    'nav.admin': 'Beheer',
+    'nav.account': 'Account',
+    'nav.profile': 'Profiel',
+    'nav.favorites': 'Favorieten',
+    'nav.new_post': 'Nieuwe post',
+    'nav.language': 'Taal',
+    'switch.agenda': 'Agenda',
+    'switch.solo': 'Solo',
+    'switch.circle': 'Cirkel',
+    'switch.timeline': 'Tijdlijn',
+    'switch.grid': 'Grid',
+    'postnav.newer': 'Nieuwer',
+    'postnav.older': 'Ouder',
+    'postnav.newest': 'Nieuwste post',
+    'postnav.oldest': 'Oudste post',
+    'footer.subscribe_cta': 'Blijf op de hoogte',
+    'footer.subscribe': 'Inschrijven',
+    'footer.install': 'Installeer app',
+    'common.email_placeholder': 'jouw@email.nl',
+  },
+  en: {
+    'nav.back_to_site': '← Back to site',
+    'nav.home': 'Home',
+    'nav.archive': 'Archive',
+    'nav.search': 'Search',
+    'nav.theme': 'Toggle theme',
+    'nav.install': 'Install app',
+    'nav.login': 'Log in',
+    'nav.logout': 'Log out',
+    'nav.admin': 'Admin',
+    'nav.account': 'Account',
+    'nav.profile': 'Profile',
+    'nav.favorites': 'Favorites',
+    'nav.new_post': 'New post',
+    'nav.language': 'Language',
+    'switch.agenda': 'Agenda',
+    'switch.solo': 'Solo',
+    'switch.circle': 'Circle',
+    'switch.timeline': 'Timeline',
+    'switch.grid': 'Grid',
+    'postnav.newer': 'Newer',
+    'postnav.older': 'Older',
+    'postnav.newest': 'Newest post',
+    'postnav.oldest': 'Oldest post',
+    'footer.subscribe_cta': 'Stay in the loop',
+    'footer.subscribe': 'Subscribe',
+    'footer.install': 'Install app',
+    'common.email_placeholder': 'you@email.com',
+  },
+  de: {
+    'nav.back_to_site': '← Zurück zur Seite',
+    'nav.home': 'Start',
+    'nav.archive': 'Archiv',
+    'nav.search': 'Suche',
+    'nav.theme': 'Thema wechseln',
+    'nav.install': 'App installieren',
+    'nav.login': 'Anmelden',
+    'nav.logout': 'Abmelden',
+    'nav.admin': 'Verwaltung',
+    'nav.account': 'Konto',
+    'nav.profile': 'Profil',
+    'nav.favorites': 'Favoriten',
+    'nav.new_post': 'Neuer Beitrag',
+    'nav.language': 'Sprache',
+    'switch.agenda': 'Termine',
+    'switch.solo': 'Solo',
+    'switch.circle': 'Kreis',
+    'switch.timeline': 'Zeitleiste',
+    'switch.grid': 'Raster',
+    'postnav.newer': 'Neuer',
+    'postnav.older': 'Älter',
+    'postnav.newest': 'Neuester Beitrag',
+    'postnav.oldest': 'Ältester Beitrag',
+    'footer.subscribe_cta': 'Bleib auf dem Laufenden',
+    'footer.subscribe': 'Abonnieren',
+    'footer.install': 'App installieren',
+    'common.email_placeholder': 'du@email.de',
+  },
+};
+
+export function t(lang, key, vars) {
+  const l = SUPPORTED.includes(lang) ? lang : 'nl';
+  let s = (DICT[l] && DICT[l][key]);
+  if (s === undefined) s = (DICT.nl[key] !== undefined ? DICT.nl[key] : key);
+  if (vars) for (const k in vars) s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), vars[k]);
+  return s;
+}
+
+// Bepaal de taal voor dit request: sessie-keuze → browser-taal → nl.
+export function resolveLang(req) {
+  const s = req && req.session && req.session.lang;
+  if (s && SUPPORTED.includes(s)) return s;
+  const al = ((req && req.headers && req.headers['accept-language']) || '').toLowerCase();
+  const first = al.split(',')[0].trim().slice(0, 2);
+  if (SUPPORTED.includes(first)) return first;
+  return 'nl';
+}
Index: src/views/partials/footer.ejs
===================================================================
--- src/views/partials/footer.ejs	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/views/partials/footer.ejs	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -2,8 +2,8 @@
   <% if (typeof footerNewsletter !== 'undefined' && footerNewsletter && (typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof site !== 'undefined' && site) { %>
     <div class="container footer-nl">
-      <span class="footer-nl-label">📬 Blijf op de hoogte</span>
+      <span class="footer-nl-label">📬 <%= t('footer.subscribe_cta') %></span>
       <form method="post" action="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief" class="footer-nl-form">
-        <input type="email" name="email" required placeholder="jouw@email.nl" autocomplete="email" aria-label="E-mailadres">
-        <button type="submit">Inschrijven</button>
+        <input type="email" name="email" required placeholder="<%= t('common.email_placeholder') %>" autocomplete="email" aria-label="E-mailadres">
+        <button type="submit"><%= t('footer.subscribe') %></button>
       </form>
     </div>
@@ -17,7 +17,13 @@
     <button type="button" class="install-app-btn install-app-btn--footer" data-pcms-install-app>
       <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 20h14v-2H5v2zm7-18L5.33 9h3.84v6h5.66V9h3.84L12 2z" fill="currentColor"/></svg>
-      <span>Installeer app</span>
+      <span><%= t('footer.install') %></span>
     </button>
     <div class="footer-meta">
+      <% if (typeof langs !== 'undefined' && langs && langs.length > 1) { %>
+        <select class="footer-lang" aria-label="<%= t('nav.language') %>"
+                onchange="location.href='/lang/'+this.value+'?r='+encodeURIComponent(location.pathname+location.search)">
+          <% langs.forEach(function(l){ %><option value="<%= l.code %>"<%= l.active ? ' selected' : '' %>><%= l.name %></option><% }); %>
+        </select>
+      <% } %>
       <a class="footer-version" href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/changelog" title="Bekijk wat er per release veranderd is">Klonkt Beta<%= (typeof appVersion !== 'undefined' && appVersion) ? ' v' + appVersion : '' %></a>
       <%# Mobile-only theme toggle. On desktop the topnav has its own toggle
@@ -101,4 +107,11 @@
   display: inline-flex; align-items: center; gap: 0.75rem;
 }
+.footer-lang {
+  background: transparent; color: var(--ink); opacity: 1;
+  border: 1px solid var(--rule); border-radius: 999px;
+  padding: 0.3rem 0.5rem; font-size: 0.78rem; cursor: pointer;
+  font-family: inherit;
+}
+.footer-lang:hover { border-color: var(--accent); }
 .footer-version { color: inherit; text-decoration: none; }
 .footer-version:hover { color: var(--accent); text-decoration: underline; }
Index: src/views/partials/post-nav.ejs
===================================================================
--- src/views/partials/post-nav.ejs	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/views/partials/post-nav.ejs	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -8,5 +8,5 @@
          hx-get="/<%= newerPost.slug %>?partial=1" hx-target="#pcms-main"
          hx-push-url="/<%= newerPost.slug %>" hx-indicator="#pcms-loading"<% } %>>
-        <span class="post-nav-label"><% if (newerPost.pinned) { %><span class="post-nav-pin" title="Vastgepind">📌</span> <% } %>← Newer</span>
+        <span class="post-nav-label"><% if (newerPost.pinned) { %><span class="post-nav-pin" title="Vastgepind">📌</span> <% } %>← <%= t('postnav.newer') %></span>
         <span class="post-nav-title"><%= newerPost.title %></span>
       </a>
@@ -14,5 +14,5 @@
       <div class="post-nav-prev is-empty" aria-hidden="true">
         <span class="post-nav-label">← Newer</span>
-        <span class="post-nav-title">Nieuwste post</span>
+        <span class="post-nav-title"><%= t('postnav.newest') %></span>
       </div>
     <% } %>
@@ -21,5 +21,5 @@
          hx-get="/<%= olderPost.slug %>?partial=1" hx-target="#pcms-main"
          hx-push-url="/<%= olderPost.slug %>" hx-indicator="#pcms-loading"<% } %>>
-        <span class="post-nav-label">Older →<% if (olderPost.pinned) { %> <span class="post-nav-pin" title="Vastgepind">📌</span><% } %></span>
+        <span class="post-nav-label"><%= t('postnav.older') %> →<% if (olderPost.pinned) { %> <span class="post-nav-pin" title="Vastgepind">📌</span><% } %></span>
         <span class="post-nav-title"><%= olderPost.title %></span>
       </a>
@@ -27,5 +27,5 @@
       <div class="post-nav-next is-empty" aria-hidden="true">
         <span class="post-nav-label">Older →</span>
-        <span class="post-nav-title">Oudste post</span>
+        <span class="post-nav-title"><%= t('postnav.oldest') %></span>
       </div>
     <% } %>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/views/partials/topnav.ejs	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -26,5 +26,5 @@
     <div class="masthead-brand">
       <% if (_isAdmin) { %>
-        <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin">← Terug naar site</a>
+        <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
       <% } else if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
         <%# Hub: de brand gaat naar de hub-home via HTMX. De chrome wordt in hub-modus
@@ -64,5 +64,5 @@
            hx-indicator="#pcms-loading">
           <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8v13H3V8"/><rect x="1" y="3" width="22" height="5" rx="1"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
-          <span class="nav-label">Archief</span>
+          <span class="nav-label"><%= t('nav.archive') %></span>
         </a>
       <% } %>
@@ -118,5 +118,5 @@
                  hx-push-url="/account" hx-indicator="#pcms-loading">
                 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
-                <span>Account</span>
+                <span><%= t('nav.account') %></span>
               </a>
               <a href="/favorieten" role="menuitem" class="udi"
@@ -124,5 +124,5 @@
                  hx-push-url="/favorieten" hx-indicator="#pcms-loading">
                 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
-                <span>Favorieten</span>
+                <span><%= t('nav.favorites') %></span>
               </a>
               <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
@@ -131,5 +131,5 @@
                    hx-push-url="/admin" hx-indicator="#pcms-loading">
                   <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3z"/><path d="M14 3h7v7h-7z"/><path d="M14 14h7v7h-7z"/><path d="M3 14h7v7H3z"/></svg>
-                  <span>Beheer</span>
+                  <span><%= t('nav.admin') %></span>
                 </a>
               <% } %>
@@ -139,5 +139,5 @@
                    hx-push-url="<%= _siteUrlBase %>/posts/new" hx-indicator="#pcms-loading">
                   <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
-                  <span>Nieuwe post</span>
+                  <span><%= t('nav.new_post') %></span>
                 </a>
               <% } %>
@@ -147,5 +147,5 @@
               <a href="/auth/logout" role="menuitem" class="udi udi-danger">
                 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
-                <span>Uitloggen</span>
+                <span><%= t('nav.logout') %></span>
               </a>
             </div>
@@ -156,5 +156,5 @@
            hx-get="/auth/login?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
            hx-push-url="/auth/login" hx-indicator="#pcms-loading">
-          <span class="nav-label">Inloggen</span>
+          <span class="nav-label"><%= t('nav.login') %></span>
         </a>
       <% } %>
Index: src/views/partials/view-switcher.ejs
===================================================================
--- src/views/partials/view-switcher.ejs	(revision d0f4c1011b0743745d322f1913c61d899e5525a1)
+++ src/views/partials/view-switcher.ejs	(revision 03fa5481c4f9961f422ed9a06824e8944fb868bf)
@@ -19,5 +19,5 @@
       <a class="vs-agenda<%= _onShows ? ' is-active' : '' %>" href="<%= _sbA %>/shows" aria-label="Agenda — evenementen">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
-        <span>Agenda</span>
+        <span><%= t('switch.agenda') %></span>
       </a>
     </div>
@@ -29,7 +29,7 @@
     <div class="feed-scope" role="tablist" aria-label="Solo of cirkel">
       <a class="feed-scope-btn<%= (!_neutral && !_onCirkel) ? ' is-active' : '' %>" href="<%= _sb %>/"
-         role="tab" aria-selected="<%= (!_neutral && !_onCirkel) ? 'true' : 'false' %>">Solo</a>
+         role="tab" aria-selected="<%= (!_neutral && !_onCirkel) ? 'true' : 'false' %>"><%= t('switch.solo') %></a>
       <a class="feed-scope-btn<%= (!_neutral && _onCirkel) ? ' is-active' : '' %>" href="<%= _sb %>/cirkel"
-         role="tab" aria-selected="<%= (!_neutral && _onCirkel) ? 'true' : 'false' %>">Cirkel</a>
+         role="tab" aria-selected="<%= (!_neutral && _onCirkel) ? 'true' : 'false' %>"><%= t('switch.circle') %></a>
     </div>
     <% } %>
@@ -38,10 +38,10 @@
               aria-selected="<%= (!_neutral && !(typeof site !== 'undefined' && site && site.feed_view_default === 'grid')) ? 'true' : 'false' %>">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
-        <span>Tijdlijn</span>
+        <span><%= t('switch.timeline') %></span>
       </button>
       <button type="button" class="view-switch-btn" data-view="grid" role="tab"
               aria-selected="<%= (!_neutral && (typeof site !== 'undefined' && site && site.feed_view_default === 'grid')) ? 'true' : 'false' %>">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
-        <span>Grid</span>
+        <span><%= t('switch.grid') %></span>
       </button>
     </div>
