Changeset bbf9d1a in Klonkt for src/views/pages


Ignore:
Timestamp:
06/21/2026 02:32:39 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
4885eab
Parents:
929d98d
Message:

feat(i18n phase 5): all admin sub-pages translated (NL/EN/DE)

settings, site-edit, seo, google, audio, stats, playlists, users, sites,
comments, circle, shows, newsletter, updates, epk, help. ~520 new keys
per language. Loop-shadowing of t() resolved by fetching before the loop. 709
view-keys cross-checked against the dictionary (0 missing).

Co-Authored-By: Claude <noreply@…>

Location:
src/views/pages
Files:
16 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/admin-audio.ejs

    r929d98d rbbf9d1a  
    44  <header class="ax-header">
    55    <div>
    6       <h1>Audio tracks</h1>
    7       <p class="ax-tagline">Site-level MP3s. Gebruik <code>[[track:&lt;id&gt;]]</code> in een post om een play-knop in te voegen.</p>
     6      <h1><%= t('aaud.title') %></h1>
     7      <p class="ax-tagline"><%= t('aaud.tagline_pre') %> <code>[[track:&lt;id&gt;]]</code> <%= t('aaud.tagline_post') %></p>
    88    </div>
    99  </header>
     
    1414  <%# ── UPLOAD ──────────────────────────────────────────────── %>
    1515  <section class="ax-card">
    16     <div class="ax-card-title">Upload</div>
     16    <div class="ax-card-title"><%= t('aaud.upload') %></div>
    1717
    1818    <%# Bulk drag-drop zone. Multiple files at once, transcoded sequentially.
     
    2525      <div class="ax-row ax-row-2">
    2626        <label class="ax-field">
    27           <span>Artiest <small>(toegepast op alle bestanden)</small></span>
    28           <input type="text" id="batch-artist" placeholder="Optioneel">
     27          <span><%= t('aaud.artist') %> <small><%= t('aaud.applied_all') %></small></span>
     28          <input type="text" id="batch-artist" placeholder="<%= t('aaud.optional') %>">
    2929        </label>
    3030        <label class="ax-field">
    31           <span>Album <small>(toegepast op alle bestanden)</small></span>
    32           <input type="text" id="batch-album" placeholder="Optioneel">
     31          <span><%= t('aaud.album') %> <small><%= t('aaud.applied_all') %></small></span>
     32          <input type="text" id="batch-album" placeholder="<%= t('aaud.optional') %>">
    3333        </label>
    3434      </div>
    3535      <label class="ax-field ax-file">
    36         <span>Cover <small>(optioneel, toegepast op alle bestanden — jpg/png/webp/gif, max 5 MB)</small></span>
     36        <span><%= t('aaud.cover') %> <small><%= t('aaud.cover_hint') %></small></span>
    3737        <span class="ax-file-control">
    38           <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 Kies cover</span>
    39           <span class="ax-file-name" data-empty>Geen bestand gekozen</span>
     38          <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 <%= t('aaud.choose_cover') %></span>
     39          <span class="ax-file-name" data-empty><%= t('aaud.no_file') %></span>
    4040          <input type="file" id="batch-cover" accept="image/jpeg,image/png,image/webp,image/gif">
    4141        </span>
     
    4545      <label class="ax-dropzone" id="audio-dropzone" tabindex="0">
    4646        <span class="ax-dropzone-icon">🎵</span>
    47         <strong class="ax-dropzone-title">Sleep audio hierheen</strong>
    48         <span class="ax-dropzone-sub">of klik om bestanden te kiezen<br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span>
     47        <strong class="ax-dropzone-title"><%= t('aaud.drag_here') %></strong>
     48        <span class="ax-dropzone-sub"><%= t('aaud.or_click') %><br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span>
    4949        <input type="file" id="audio-files" accept="audio/*" multiple>
    5050      </label>
     
    5454
    5555      <div class="ax-form-actions" id="upload-actions" hidden>
    56         <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ Start upload</button>
    57         <button type="button" class="ax-btn" id="clear-queue-btn">Wis lijst</button>
     56        <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ <%= t('aaud.start_upload') %></button>
     57        <button type="button" class="ax-btn" id="clear-queue-btn"><%= t('aaud.clear_list') %></button>
    5858      </div>
    5959    </div>
     
    6363  <section class="ax-card">
    6464    <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem">
    65       Tracks <span class="ax-count"><%= tracks.length %></span>
     65      <%= t('aaud.tracks') %> <span class="ax-count"><%= tracks.length %></span>
    6666      <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn"
    67               style="margin-left:auto;font-size:.85rem" title="Een track zonder audiobestand — alleen titel + open-in links">
    68         + Track zonder audio
     67              style="margin-left:auto;font-size:.85rem" title="<%= t('aaud.add_link_track_title') %>">
     68        + <%= t('aaud.add_link_track') %>
    6969      </button>
    7070    </div>
     
    7373      <div class="ax-empty">
    7474        <div class="ax-empty-icon">♫</div>
    75         <p>Nog geen tracks. Upload er een hierboven.</p>
     75        <p><%= t('aaud.no_tracks') %></p>
    7676      </div>
    7777    <% } else { %>
     78      <%
     79        // Pre-fetch loop labels — `t` is shadowed by the loop var below.
     80        var _aaudUntitled = t('aaud.untitled');
     81        var _aaudPlay     = t('aaud.play');
     82        var _aaudEdit     = t('aaud.edit');
     83        var _aaudDlOn     = t('aaud.dl_on');
     84        var _aaudDlOff    = t('aaud.dl_off');
     85        var _aaudDelete   = t('aaud.delete');
     86        var _aaudCopy     = t('aaud.copy_click');
     87        var _aaudDelConfirm = t('aaud.delete_confirm');
     88      %>
    7889      <ul class="ax-list">
    7990        <% tracks.forEach(function(t) { %>
     
    8697
    8798            <div class="ax-track-meta">
    88               <div class="ax-track-title" data-cell="title"><%= t.title || '(zonder titel)' %></div>
     99              <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div>
    89100              <div class="ax-track-sub">
    90101                <span data-cell="artist"><%= t.artist || '—' %></span>
     
    92103                <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span>
    93104              </div>
    94               <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="Klik om te kopiëren">[[track:<%= t.id %>]]</code>
     105              <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code>
    95106            </div>
    96107
     
    100111                        data-stream-url="<%= t.stream_url %>"
    101112                        data-track-id="<%= t.id %>"
    102                         aria-label="Afspelen" title="Afspelen">
     113                        aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>">
    103114                  <span class="ax-track-play-icon" aria-hidden="true">▶</span>
    104115                </button>
    105116              <% } %>
    106               <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="Bewerken" title="Bewerken">✎</button>
     117              <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button>
    107118              <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
    108119                <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>"
    109                         aria-label="Download-voor-email" title="<%= t.downloadable ? 'Download-voor-email staat AAN — klik om uit te zetten' : 'Download-voor-email staat uit — klik om aan te zetten' %>"
     120                        aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>"
    110121                        style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
    111122              <% } %>
    112               <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm('Track verwijderen?')" class="ax-track-delete">
    113                 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
     123              <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm('<%= _aaudDelConfirm %>')" class="ax-track-delete">
     124                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
    114125              </form>
    115126            </div>
     
    122133  <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %>
    123134    <section class="ax-section" style="margin-top:1.5rem">
    124       <h2 style="margin:0 0 .4rem">🔊 Embedbare speler</h2>
    125       <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem">Plak deze code op je eigen website/blog om je muziek met deze speler in te sluiten:</p>
     135      <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2>
     136      <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p>
    126137      <textarea readonly rows="3" onclick="this.select()" style="width:100%;font-family:monospace;font-size:12px;padding:10px;border-radius:8px;border:1px solid var(--rule);background:var(--paper-2);color:inherit;">&lt;iframe src="<%= embedUrl %>" width="100%" height="420" frameborder="0" loading="lazy" style="border-radius:12px"&gt;&lt;/iframe&gt;</textarea>
    127       <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ Speler-voorbeeld openen</a></p>
     138      <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p>
    128139    </section>
    129140  <% } %>
     
    573584        nameEl.removeAttribute('data-empty');
    574585      } else {
    575         nameEl.textContent = 'Geen bestand gekozen';
     586        nameEl.textContent = '<%= t('aaud.no_file') %>';
    576587        nameEl.setAttribute('data-empty', '');
    577588      }
     
    592603      if (icon) icon.textContent = playing ? '⏸' : '▶';
    593604      btn.classList.toggle('is-playing', playing);
    594       btn.setAttribute('aria-label', playing ? 'Pauzeren' : 'Afspelen');
     605      btn.setAttribute('aria-label', playing ? '<%= t('aaud.pause') %>' : '<%= t('aaud.play') %>');
    595606    }
    596607
     
    695706  function setItemStatus(item, status, label) {
    696707    const labels = {
    697       queued:      'Wachten',
    698       uploading:   'Uploaden…',
    699       transcoding: 'Converteren…',
    700       done:        '✓ Klaar',
    701       error:       '✗ Fout',
     708      queued:      '<%= t('aaud.st_queued') %>',
     709      uploading:   '<%= t('aaud.st_uploading') %>',
     710      transcoding: '<%= t('aaud.st_transcoding') %>',
     711      done:        '✓ <%= t('aaud.st_done') %>',
     712      error:       '✗ <%= t('aaud.st_error') %>',
    702713    };
    703714    item.status = status;
     
    726737      li.innerHTML =
    727738        '<div class="ax-queue-name"></div>' +
    728         '<span class="ax-queue-status ax-queue-status--queued">Wachten</span>';
     739        '<span class="ax-queue-status ax-queue-status--queued"><%= t('aaud.st_queued') %></span>';
    729740      // Use textContent to avoid HTML-injection if a filename contains markup.
    730741      li.querySelector('.ax-queue-name').textContent = file.name;
     
    813824      } catch (err) {
    814825        console.error('upload failed for', item.file.name, err);
    815         setItemStatus(item, 'error', '✗ ' + (err.message || 'Mislukt'));
     826        setItemStatus(item, 'error', '✗ ' + (err.message || '<%= t('aaud.failed') %>'));
    816827      }
    817828    }
     
    864875      let data;
    865876      try { data = await res.json(); }
    866       catch (_) { throw new Error('Onverwacht serverantwoord (' + res.status + ')'); }
     877      catch (_) { throw new Error('<%= t('aaud.err_unexpected') %> (' + res.status + ')'); }
    867878
    868879      if (!res.ok || !data.ok) {
     
    870881      }
    871882
    872       setItemStatus(item, 'done', '✓ ' + (data.title || 'Klaar'));
     883      setItemStatus(item, 'done', '✓ ' + (data.title || '<%= t('aaud.st_done') %>'));
    873884    } catch (err) {
    874885      clearTimeout(transcodeHint);
     
    885896        el.classList.add('is-copied');
    886897        const original = el.textContent;
    887         el.textContent = '✓ gekopieerd';
     898        el.textContent = '✓ <%= t('aaud.copied') %>';
    888899        setTimeout(() => {
    889900          el.classList.remove('is-copied');
     
    903914          method: 'POST', credentials: 'same-origin',
    904915          headers: { 'Content-Type': 'application/json' },
    905           body: JSON.stringify({ title: 'Nieuwe track' }),
     916          body: JSON.stringify({ title: '<%= t('aaud.new_track') %>' }),
    906917        });
    907918        const j = await r.json();
    908         if (!r.ok || !j.ok) throw new Error(j.error || 'Aanmaken mislukt');
     919        if (!r.ok || !j.ok) throw new Error(j.error || '<%= t('aaud.create_failed') %>');
    909920        if (typeof window.openTrackEditor !== 'function') { location.reload(); return; }
    910921        window.openTrackEditor({ id: j.id, onSaved: () => location.reload() });
    911922      } catch (err) {
    912         alert('Track aanmaken mislukt: ' + err.message);
     923        alert('<%= t('aaud.create_failed') %>: ' + err.message);
    913924      } finally {
    914925        addLinkBtn.disabled = false;
     
    923934    btn.addEventListener('click', () => {
    924935      if (typeof window.openTrackEditor !== 'function') {
    925         alert('Track editor niet geladen');
     936        alert('<%= t('aaud.editor_not_loaded') %>');
    926937        return;
    927938      }
     
    936947          const artistEl = row.querySelector('[data-cell="artist"]');
    937948          const albumEl  = row.querySelector('[data-cell="album"]');
    938           if (titleEl)  titleEl.textContent  = track.title  || '(zonder titel)';
     949          if (titleEl)  titleEl.textContent  = track.title  || '<%= t('aaud.untitled') %>';
    939950          if (artistEl) artistEl.textContent = track.artist || '—';
    940951          if (albumEl)  {
     
    984995        btn.style.opacity = want ? '1' : '.5';
    985996        btn.title = want
    986           ? 'Download-voor-email staat AAN — klik om uit te zetten'
    987           : 'Download-voor-email staat uit — klik om aan te zetten';
     997          ? '<%= t('aaud.dl_on') %>'
     998          : '<%= t('aaud.dl_off') %>';
    988999      } catch (e) {
    989         alert('Kon niet wijzigen: ' + (e.message || e));
     1000        alert('<%= t('aaud.change_failed') %>: ' + (e.message || e));
    9901001      } finally {
    9911002        btn.disabled = false;
  • src/views/pages/admin-circle.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page">
    2   <h1>Cirkel</h1>
    3   <p><a href="/admin/settings" class="btn">&larr; Instellingen</a></p>
     2  <h1><%= t('acir.title') %></h1>
     3  <p><a href="/admin/settings" class="btn">&larr; <%= t('acir.back_settings') %></a></p>
    44
    55  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
     
    88  <% if (tenancy !== 'circle') { %>
    99    <section class="set-card">
    10       <p class="set-help">De modus staat niet op <strong>Cirkels</strong>. Zet 'm aan bij
    11         <a href="/admin/settings">Instellingen</a> om je cirkel-feed te tonen op <code>/cirkel</code>.
    12         Je kunt hieronder al wel bronnen klaarzetten.</p>
     10      <p class="set-help"><%= t('acir.mode_off_1') %> <strong><%= t('acir.circles') %></strong>. <%= t('acir.mode_off_2') %>
     11        <a href="/admin/settings"><%= t('acir.settings') %></a> <%= t('acir.mode_off_3') %> <code>/cirkel</code>.
     12        <%= t('acir.mode_off_4') %></p>
    1313    </section>
    1414  <% } %>
    1515
    1616  <section class="set-card" style="margin-top:1rem">
    17     <h2>Mijn zichtbaarheid</h2>
    18     <p class="set-help">Doe je mee aan cirkels? Dit maakt je <strong>al-publieke</strong> posts
    19       ophaalbaar voor andere Klonkt-sites via een ondertekende feed
    20       (<code>/.klonkt/outbox.json</code>). Het is een deelname-keuze, geen privacy-slot:
    21       je posts blijven sowieso openbaar op je site, ook als dit uit staat. Wil je iets
    22       afschermen, maak die post dan niet-publiek.</p>
     17    <h2><%= t('acir.visibility_title') %></h2>
     18    <p class="set-help"><%= t('acir.visibility_help_1') %> <strong><%= t('acir.all_public') %></strong> <%= t('acir.visibility_help_2') %>
     19      (<code>/.klonkt/outbox.json</code>). <%= t('acir.visibility_help_3') %></p>
    2320    <form method="post" action="/admin/circle/allow" class="set-form">
    2421      <label class="set-opt" style="align-items:center">
    2522        <input type="checkbox" name="allow_circle" value="on" <%= allowCircle ? 'checked' : '' %>>
    26         <span><strong>Toon mijn site in cirkels van anderen</strong></span>
     23        <span><strong><%= t('acir.show_in_circles') %></strong></span>
    2724      </label>
    28       <button type="submit" class="btn btn-primary">Opslaan</button>
     25      <button type="submit" class="btn btn-primary"><%= t('acir.save') %></button>
    2926    </form>
    3027  </section>
    3128
    3229  <section class="set-card" style="margin-top:1rem">
    33     <h2>Klonkt-site toevoegen</h2>
    34     <p class="set-help">Plak de basis-URL van een andere Klonkt-site. Asymmetrisch: jij toont
    35       hen, los van of zij jou tonen.</p>
     30    <h2><%= t('acir.add_title') %></h2>
     31    <p class="set-help"><%= t('acir.add_help') %></p>
    3632    <form method="post" action="/admin/circle/add" class="set-form">
    3733      <label class="set-field">
    38         <span>URL</span>
     34        <span><%= t('acir.url') %></span>
    3935        <input type="url" name="remote_url" placeholder="https://artiest.klonkt.com" required>
    4036      </label>
    4137      <label class="set-field">
    42         <span>Label <small style="font-weight:400">— optioneel</small></span>
    43         <input type="text" name="label" maxlength="80" placeholder="bijv. Joost Klein">
     38        <span><%= t('acir.label') %> <small style="font-weight:400">— <%= t('acir.optional') %></small></span>
     39        <input type="text" name="label" maxlength="80" placeholder="<%= t('acir.label_ph') %>">
    4440      </label>
    45       <button type="submit" class="btn btn-primary">Toevoegen</button>
     41      <button type="submit" class="btn btn-primary"><%= t('acir.add') %></button>
    4642    </form>
    4743  </section>
    4844
    4945  <section class="set-card" style="margin-top:1rem">
    50     <h2>In mijn cirkel (<%= links.length %>)</h2>
     46    <h2><%= t('acir.in_circle', { n: links.length }) %></h2>
    5147    <% if (!links.length) { %>
    52       <p class="set-help">Nog geen bronnen. Voeg er hierboven een toe.</p>
     48      <p class="set-help"><%= t('acir.no_sources') %></p>
    5349    <% } else { %>
    5450      <form method="post" action="/admin/circle/sync-all" style="margin:0 0 .9rem">
    55         <button type="submit" class="btn">&#8635; Alles nu synchroniseren</button>
     51        <button type="submit" class="btn">&#8635; <%= t('acir.sync_all') %></button>
    5652      </form>
     53      <%
     54        var _cActive = t('acir.st_active');
     55        var _cPosts = t('acir.posts');
     56        var _cLast = t('acir.last');
     57        var _cMismatch = t('acir.st_mismatch');
     58        var _cMismatchReason = t('acir.mismatch_reason');
     59        var _cError = t('acir.st_error');
     60        var _cPaused = t('acir.st_paused');
     61        var _cRefresh = t('acir.refresh');
     62        var _cRemove = t('acir.remove');
     63        var _cRemoveConfirm = t('acir.remove_confirm');
     64      %>
    5765      <ul class="circ-list">
    5866        <% links.forEach(function(l){ %>
     
    6371              <div class="circ-status">
    6472                <% if (l.status === 'active') { %>
    65                   <span class="circ-badge ok">&#10003; actief</span>
    66                   <span class="circ-meta"><%= counts[l.id] || 0 %> posts<% if (l.last_synced) { %> &middot; laatst: <%= l.last_synced %><% } %></span>
     73                  <span class="circ-badge ok">&#10003; <%= _cActive %></span>
     74                  <span class="circ-meta"><%= counts[l.id] || 0 %> <%= _cPosts %><% if (l.last_synced) { %> &middot; <%= _cLast %>: <%= l.last_synced %><% } %></span>
    6775                <% } else if (l.status === 'outdated') { %>
    68                   <span class="circ-badge err">&#8635; versie-mismatch</span>
    69                   <div class="circ-reason"><%= l.last_error || 'Deze site draait een andere Klonkt-protocolversie — bijwerken nodig om te federeren.' %></div>
     76                  <span class="circ-badge err">&#8635; <%= _cMismatch %></span>
     77                  <div class="circ-reason"><%= l.last_error || _cMismatchReason %></div>
    7078                <% } else if (l.status === 'error') { %>
    71                   <span class="circ-badge err">&#9888; fout</span>
     79                  <span class="circ-badge err">&#9888; <%= _cError %></span>
    7280                  <% if (l.last_error) { %><div class="circ-reason"><%= l.last_error %></div><% } %>
    7381                <% } else if (l.status === 'paused') { %>
    74                   <span class="circ-badge">&#9208; gepauzeerd</span>
     82                  <span class="circ-badge">&#9208; <%= _cPaused %></span>
    7583                <% } else { %>
    7684                  <%# Onbekende status: toon 'm rauw + eventuele reden i.p.v. stil "gepauzeerd". %>
     
    8290            <div class="circ-actions">
    8391              <form method="post" action="/admin/circle/<%= l.id %>/sync" style="display:inline">
    84                 <button type="submit" class="btn">Verversen</button>
     92                <button type="submit" class="btn"><%= _cRefresh %></button>
    8593              </form>
    8694              <form method="post" action="/admin/circle/<%= l.id %>/remove" style="display:inline"
    87                     onsubmit="return confirm('Verwijderen uit je cirkel?')">
    88                 <button type="submit" class="btn btn-danger">Verwijder</button>
     95                    onsubmit="return confirm('<%= _cRemoveConfirm %>')">
     96                <button type="submit" class="btn btn-danger"><%= _cRemove %></button>
    8997              </form>
    9098            </div>
  • src/views/pages/admin-comments.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-comments-page">
    2   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    3   <h1>Comment moderation</h1>
     2  <p><a href="/admin" class="btn">&larr; <%= t('acom.back') %></a></p>
     3  <h1><%= t('acom.title') %></h1>
    44
    55  <p class="muted">
    6     Mode for this site: <strong><%= moderationMode %></strong>
     6    <%= t('acom.mode_for_site') %> <strong><%= moderationMode %></strong>
    77    <% if (moderationMode === 'trust') { %>
    8       &mdash; comments are auto-approved. Switch to <em>moderate</em> in
    9       <a href="/admin/sites/<%= site.slug %>/edit">site settings</a> to queue them.
     8      &mdash; <%= t('acom.mode_trust_a') %> <em><%= t('acom.mode_moderate_word') %></em>
     9      <a href="/admin/sites/<%= site.slug %>/edit"><%= t('acom.site_settings') %></a> <%= t('acom.mode_trust_b') %>
    1010    <% } else { %>
    11       &mdash; new comments require approval before showing up on posts.
     11      &mdash; <%= t('acom.mode_moderate_hint') %>
    1212    <% } %>
    1313  </p>
     
    1616  <% if (error)   { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %>
    1717
    18   <h2>Pending (<%= pending.length %>)</h2>
     18  <h2><%= t('acom.pending', { n: pending.length }) %></h2>
    1919  <% if (!pending.length) { %>
    20     <p class="muted">Nothing waiting.</p>
     20    <p class="muted"><%= t('acom.nothing_waiting') %></p>
    2121  <% } else { %>
     22    <%
     23      var _c_reply    = t('acom.reply');
     24      var _c_on       = t('acom.on');
     25      var _c_approve  = t('acom.approve');
     26      var _c_reject   = t('acom.reject');
     27    %>
    2228    <ol class="moderation-list">
    2329      <% pending.forEach(function(c) { %>
     
    2531          <div class="moderation-meta">
    2632            <strong><%= c.author_username %></strong>
    27             <% if (c.parent_comment_id) { %><span class="muted">(reply)</span><% } %>
    28             on <a href="<%= siteUrlBase %>/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a>
     33            <% if (c.parent_comment_id) { %><span class="muted">(<%= _c_reply %>)</span><% } %>
     34            <%= _c_on %> <a href="<%= siteUrlBase %>/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a>
    2935            <span class="muted">&middot; <%= formatDateTime(c.created_at) %></span>
    3036          </div>
     
    3238          <div class="moderation-actions">
    3339            <form method="post" action="<%= siteUrlBase %>/admin/comments/<%= c.id %>/approve" style="display:inline">
    34               <button type="submit" class="btn btn-primary">Approve</button>
     40              <button type="submit" class="btn btn-primary"><%= _c_approve %></button>
    3541            </form>
    3642            <form method="post" action="<%= siteUrlBase %>/admin/comments/<%= c.id %>/reject" style="display:inline">
    37               <button type="submit" class="btn btn-danger">Reject</button>
     43              <button type="submit" class="btn btn-danger"><%= _c_reject %></button>
    3844            </form>
    3945          </div>
     
    4349  <% } %>
    4450
    45   <h2>Recent decisions</h2>
     51  <h2><%= t('acom.recent') %></h2>
    4652  <% if (!recent.length) { %>
    47     <p class="muted">Nothing yet.</p>
     53    <p class="muted"><%= t('acom.nothing_yet') %></p>
    4854  <% } else { %>
    4955    <ul class="decision-list">
  • src/views/pages/admin-epk.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page">
    2   <h1>Perskit bewerken</h1>
    3   <p><a href="/admin" class="btn">&larr; Beheer</a> <a href="/pers" class="btn" target="_blank" rel="noopener">📰 Bekijk perskit</a></p>
     2  <h1><%= t('aepk.title') %></h1>
     3  <p><a href="/admin" class="btn">&larr; <%= t('aepk.back_admin') %></a> <a href="/pers" class="btn" target="_blank" rel="noopener">📰 <%= t('aepk.view_epk') %></a></p>
    44
    55  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
    66
    77  <section class="set-card">
    8     <h2>Tekst</h2>
     8    <h2><%= t('aepk.text_heading') %></h2>
    99    <p class="set-help">
    10       De perskit (<code>/pers</code>) toont je sitenaam + foto, deze bio en contact,
    11       plus automatisch je meest beluisterde nummers en recente posts. Laat de bio
    12       leeg om de site-tagline te gebruiken; laat contact leeg om niets te tonen
    13       (je login-mail wordt nooit automatisch getoond).
     10      <%= t('aepk.text_help') %>
    1411    </p>
    1512
    1613    <form method="post" action="/admin/epk" class="set-form">
    1714      <label class="set-field">
    18         <span>Pers-bio</span>
     15        <span><%= t('aepk.bio_label') %></span>
    1916        <textarea name="epk_bio" rows="5" maxlength="1000"
    20                   placeholder="Korte beschrijving van jou/het project voor pers &amp; boekers."><%= epkBio %></textarea>
     17                  placeholder="<%= t('aepk.bio_ph') %>"><%= epkBio %></textarea>
    2118      </label>
    2219      <label class="set-field">
    23         <span>Pers-contact</span>
     20        <span><%= t('aepk.contact_label') %></span>
    2421        <input type="text" name="epk_contact" maxlength="300"
    25                placeholder="bv. pers@jouwdomein.nl of een boekingslink"
     22               placeholder="<%= t('aepk.contact_ph') %>"
    2623               value="<%= epkContact %>">
    2724      </label>
    2825
    2926      <div class="set-field">
    30         <span>Nummers op de perskit <small>(kies er max <%= maxEpkTracks %>; laat leeg voor automatisch de top <%= maxEpkTracks %> meest beluisterd)</small></span>
     27        <span><%= t('aepk.tracks_label') %> <small><%= t('aepk.tracks_hint', { n: maxEpkTracks }) %></small></span>
    3128        <% if (!allTracks.length) { %>
    32           <p class="set-help" style="margin:0">Nog geen nummers — voeg eerst audio toe in Beheer → Audio.</p>
     29          <p class="set-help" style="margin:0"><%= t('aepk.no_tracks') %></p>
    3330        <% } else { %>
     31          <% var _untitled = t('aepk.untitled'); %>
    3432          <div class="epk-track-pick" data-max="<%= maxEpkTracks %>">
    3533            <% allTracks.forEach(function(t){ %>
     
    3735                <input type="checkbox" name="epk_tracks" value="<%= t.id %>" <%= chosenTracks.indexOf(t.id) >= 0 ? 'checked' : '' %>>
    3836                <span class="epk-box" aria-hidden="true">✓</span>
    39                 <span class="epk-pick-title"><%= t.title || '(zonder titel)' %></span>
     37                <span class="epk-pick-title"><%= t.title || _untitled %></span>
    4038                <% if (t.artist) { %><span class="epk-pick-artist"><%= t.artist %></span><% } %>
    4139              </label>
     
    4543      </div>
    4644
    47       <button type="submit" class="btn btn-primary">Opslaan</button>
     45      <button type="submit" class="btn btn-primary"><%= t('aepk.save') %></button>
    4846    </form>
    4947  </section>
  • src/views/pages/admin-google.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page">
    2   <h1>Google-login</h1>
    3   <p><a href="/admin/settings" class="btn">&larr; Instellingen</a></p>
     2  <h1><%= t('agoog.title') %></h1>
     3  <p><a href="/admin/settings" class="btn">&larr; <%= t('agoog.back') %></a></p>
    44
    55  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
     
    77
    88  <section class="set-card">
    9     <h2>Google-login (bezoekers)</h2>
    10     <p class="set-help">Laat bezoekers met Google inloggen om te reageren — met je <strong>eigen</strong>
    11       Google Cloud-client. Maak er een aan op
     9    <h2><%= t('agoog.card_title') %></h2>
     10    <p class="set-help"><%= t('agoog.card_help_pre') %> <strong><%= t('agoog.card_help_own') %></strong>
     11      <%= t('agoog.card_help_mid') %>
    1212      <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">console.cloud.google.com</a>
    13       (OAuth client-ID, type "Web application").
    14       <% if (google && google.configured) { %><strong>Status: ingesteld&nbsp;✓</strong><% } else { %>Nog niet ingesteld.<% } %></p>
     13      <%= t('agoog.card_help_post') %>
     14      <% if (google && google.configured) { %><strong><%= t('agoog.status_set') %></strong><% } else { %><%= t('agoog.status_unset') %><% } %></p>
    1515
    1616    <details class="set-google-guide">
    17       <summary>Hoe stel ik dit in? (stap voor stap)</summary>
     17      <summary><%= t('agoog.guide_summary') %></summary>
    1818      <ol class="set-google-steps">
    19         <li>Ga naar de <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a> en maak (of kies) een project.</li>
    20         <li><strong>APIs &amp; Services → OAuth consent screen</strong>: User type <strong>External</strong>. Voeg de scopes <code>email</code>, <code>profile</code> en <code>openid</code> toe. (In testmodus: voeg jezelf toe als testgebruiker.)</li>
    21         <li><strong>APIs &amp; Services → Credentials → Create credentials → OAuth client ID</strong>, type <strong>Web application</strong>.</li>
    22         <li>Bij <strong>Authorized redirect URIs</strong>: plak de redirect-URI hieronder (moet <strong>exact</strong> matchen).</li>
    23         <li>Kopieer de <strong>Client ID</strong> + <strong>Client Secret</strong> die je krijgt naar de velden hieronder en klik Opslaan.</li>
     19        <li><%= t('agoog.step1_pre') %> <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a> <%= t('agoog.step1_post') %></li>
     20        <li><strong>APIs &amp; Services → OAuth consent screen</strong>: <%= t('agoog.step2_usertype') %> <strong>External</strong>. <%= t('agoog.step2_scopes_pre') %> <code>email</code>, <code>profile</code> <%= t('agoog.step2_scopes_and') %> <code>openid</code> <%= t('agoog.step2_scopes_post') %> <%= t('agoog.step2_testmode') %></li>
     21        <li><strong>APIs &amp; Services → Credentials → Create credentials → OAuth client ID</strong>, <%= t('agoog.step3_type') %> <strong>Web application</strong>.</li>
     22        <li><%= t('agoog.step4_pre') %> <strong>Authorized redirect URIs</strong><%= t('agoog.step4_post') %></li>
     23        <li><%= t('agoog.step5_pre') %> <strong>Client ID</strong> + <strong>Client Secret</strong> <%= t('agoog.step5_post') %></li>
    2424      </ol>
    25       <p class="set-help" style="margin:.5rem 0 0"><strong>Let op:</strong> je hoeft <strong>geen API's aan te zetten</strong> — "Google Cloud APIs", People API e.d. zijn <em>niet</em> nodig. Inloggen werkt via OpenID Connect (alleen het toestemmingsscherm + de OAuth-client).</p>
     25      <p class="set-help" style="margin:.5rem 0 0"><strong><%= t('agoog.note_label') %></strong> <%= t('agoog.note_pre') %> <strong><%= t('agoog.note_noapi') %></strong> <%= t('agoog.note_mid') %> <em><%= t('agoog.note_not') %></em> <%= t('agoog.note_post') %></p>
    2626    </details>
    2727
    2828    <% if (google && google.redirectUri) { %>
    2929      <div class="set-redirect">
    30         <div class="set-redirect-label">Redirect-URI — kopieer deze en plak 'm in Google</div>
     30        <div class="set-redirect-label"><%= t('agoog.redirect_label') %></div>
    3131        <code class="set-redirect-url" style="user-select:all"><%= google.redirectUri %></code>
    3232        <ol class="set-redirect-steps">
    33           <li>Ga naar <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">Google Cloud Console → APIs &amp; Services → Credentials</a>.</li>
    34           <li>Open jouw <strong>OAuth client-ID</strong> (type "Web application").</li>
    35           <li>Onder <strong>Authorized redirect URIs</strong> → klik <strong>+ ADD URI</strong> en plak de regel hierboven.</li>
    36           <li>Klik <strong>Save</strong> en wacht ~1 minuut.</li>
     33          <li><%= t('agoog.rstep1_pre') %> <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">Google Cloud Console → APIs &amp; Services → Credentials</a>.</li>
     34          <li><%= t('agoog.rstep2') %></li>
     35          <li><%= t('agoog.rstep3_pre') %> <strong>Authorized redirect URIs</strong> → <%= t('agoog.rstep3_mid') %> <strong>+ ADD URI</strong> <%= t('agoog.rstep3_post') %></li>
     36          <li><%= t('agoog.rstep4_pre') %> <strong>Save</strong> <%= t('agoog.rstep4_post') %></li>
    3737        </ol>
    38         <p class="set-redirect-warn">Moet <strong>exact</strong> kloppen: <code>https://</code> (geen <code>http</code>), juiste domein, en <strong>geen</strong> schuine streep aan het eind. Anders krijg je <em>"Error 400: redirect_uri_mismatch"</em>.</p>
     38        <p class="set-redirect-warn"><%= t('agoog.warn_pre') %> <strong><%= t('agoog.warn_exact') %></strong> <%= t('agoog.warn_mid1') %> <code>https://</code> (<%= t('agoog.warn_nohttp') %> <code>http</code>)<%= t('agoog.warn_mid2') %> <strong><%= t('agoog.warn_noslash') %></strong> <%= t('agoog.warn_mid3') %> <em><%= t('agoog.warn_error') %></em>.</p>
    3939      </div>
    4040    <% } else { %>
    41       <p class="set-help" style="color:var(--accent)">Stel eerst <code>PUBLIC_BASE_URL</code> in — daar wordt de redirect-URI van afgeleid.</p>
     41      <p class="set-help" style="color:var(--accent)"><%= t('agoog.set_baseurl_pre') %> <code>PUBLIC_BASE_URL</code> <%= t('agoog.set_baseurl_post') %></p>
    4242    <% } %>
    4343
    4444    <form method="post" action="/admin/settings/google" class="set-form">
    4545      <label class="set-field">
    46         <span>Client ID</span>
     46        <span><%= t('agoog.client_id') %></span>
    4747        <input type="text" name="google_client_id" value="<%= google ? google.clientId : '' %>" placeholder="123-abc.apps.googleusercontent.com" autocomplete="off">
    4848      </label>
    4949      <label class="set-field">
    50         <span>Client Secret <% if (google && google.secretSet) { %><small style="font-weight:400">— ingesteld; leeg laten = ongewijzigd</small><% } %></span>
    51         <input type="password" name="google_client_secret" placeholder="<%= (google && google.secretSet) ? '•••••••• (ingesteld)' : 'GOCSPX-…' %>" autocomplete="off">
     50        <span><%= t('agoog.client_secret') %> <% if (google && google.secretSet) { %><small style="font-weight:400">— <%= t('agoog.client_secret_set') %></small><% } %></span>
     51        <input type="password" name="google_client_secret" placeholder="<%= (google && google.secretSet) ? t('agoog.secret_ph_set') : 'GOCSPX-…' %>" autocomplete="off">
    5252      </label>
    5353      <div class="set-actions">
    54         <button type="submit" class="btn btn-primary">Opslaan</button>
     54        <button type="submit" class="btn btn-primary"><%= t('agoog.save') %></button>
    5555        <% if (google && (google.clientId || google.secretSet)) { %>
    56           <button type="submit" name="clear" value="1" class="btn">Loskoppelen</button>
     56          <button type="submit" name="clear" value="1" class="btn"><%= t('agoog.disconnect') %></button>
    5757        <% } %>
    5858      </div>
    5959    </form>
    6060    <% if (typeof premiumEnabled !== 'undefined' && premiumEnabled && !isPremium) { %>
    61       <p class="set-help" style="margin-top:.7rem; color:var(--accent)">Premium-laag staat aan zonder Patreon — de Google-knop blijft verborgen tot premium ontgrendeld is.</p>
     61      <p class="set-help" style="margin-top:.7rem; color:var(--accent)"><%= t('agoog.premium_locked') %></p>
    6262    <% } %>
    6363  </section>
  • src/views/pages/admin-help.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page hb">
    2   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    3   <h1>Handleiding</h1>
    4   <p class="hb-intro">Uitleg van alle functies. Typ hieronder om te zoeken op een onderwerp of instructie.</p>
     2  <p><a href="/admin" class="btn">&larr; <%= t('ahelp.back') %></a></p>
     3  <h1><%= t('ahelp.title') %></h1>
     4  <p class="hb-intro"><%= t('ahelp.intro') %></p>
    55
    66  <div class="hb-search">
    7     <input type="search" id="hb-q" placeholder="Zoek… (bv. 'agenda', 'foto', 'cirkel', 'downloads')" autocomplete="off" aria-label="Zoek in de handleiding">
     7    <input type="search" id="hb-q" placeholder="<%= t('ahelp.search_placeholder') %>" autocomplete="off" aria-label="<%= t('ahelp.search_aria') %>">
    88  </div>
    99  <p class="hb-count" id="hb-count" aria-live="polite"></p>
     
    1212
    1313    <section class="hb-item">
    14       <h2>✍️ Nieuwe post schrijven</h2>
    15       <p>Beheer → <strong>Nieuwe post</strong>. Geef een titel en schrijf je inhoud. Onderaan kies je de status: <em>concept</em> (niet zichtbaar) of <em>gepubliceerd</em>. Concepten staan bovenaan in je Beheer-overzicht zodat je ze terugvindt.</p>
     14      <h2>✍️ <%= t('ahelp.s_newpost_h') %></h2>
     15      <p><%- t('ahelp.s_newpost_b') %></p>
    1616    </section>
    1717
    1818    <section class="hb-item">
    19       <h2>📝 Samenvatting &amp; Cirkel Preview</h2>
    20       <p>Het veld <strong>Samenvatting &amp; Cirkel Preview</strong> (de excerpt) is de korte previewtekst onder een post in lijsten, én de samenvatting die andere sites tonen als ze je post via een <strong>Cirkel</strong> overnemen. Laat je 'm leeg, dan wordt automatisch het begin van de post gebruikt.</p>
     19      <h2>📝 <%= t('ahelp.s_excerpt_h') %></h2>
     20      <p><%- t('ahelp.s_excerpt_b') %></p>
    2121    </section>
    2222
    2323    <section class="hb-item">
    24       <h2>📌 Post pinnen / volgorde</h2>
    25       <p>In de post-editor kun je een post <strong>pinnen</strong> met een rang (1 = bovenaan). Gepinde posts staan vooraan in de tijdlijn/grid, op volgorde van hun rang. Rang leeg of 0 = niet gepind.</p>
     24      <h2>📌 <%= t('ahelp.s_pin_h') %></h2>
     25      <p><%- t('ahelp.s_pin_b') %></p>
    2626    </section>
    2727
    2828    <section class="hb-item">
    29       <h2>🗓️ Publiceren plannen &amp; alleen voor fans <small>(premium)</small></h2>
    30       <p>In de editor kun je een <strong>publicatiedatum</strong> in de toekomst zetten — de post verschijnt dan automatisch op dat moment. Met <strong>Alleen voor fans</strong> zien niet-ingelogde bezoekers alleen een teaser + login-uitnodiging; ingelogde fans zien alles.</p>
     29      <h2>🗓️ <%= t('ahelp.s_schedule_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     30      <p><%- t('ahelp.s_schedule_b') %></p>
    3131    </section>
    3232
    3333    <section class="hb-item">
    34       <h2>🖼️ Afbeeldingen in posts</h2>
    35       <p>Afbeeldingen in de tekst en de cover-afbeelding worden altijd <strong>volledig</strong> getoond op de volledige breedte (niet bijgesneden), met hun natuurlijke hoogte.</p>
     34      <h2>🖼️ <%= t('ahelp.s_images_h') %></h2>
     35      <p><%- t('ahelp.s_images_b') %></p>
    3636    </section>
    3737
    3838    <section class="hb-item">
    39       <h2>🎵 Audio &amp; nummers toevoegen</h2>
    40       <p>Beheer → <strong>Audio</strong>. Upload een bestand of voeg een <em>link-only</em> nummer toe (zonder upload, alleen "open in"-links). Per nummer vul je titel, artiest, cover, en optioneel album/positie in. In een post toon je een nummer met de shortcode <code>[[track:id]]</code>, een album met <code>[[album:Naam]]</code>, een playlist met <code>[[playlist:id]]</code>.</p>
     39      <h2>🎵 <%= t('ahelp.s_audio_h') %></h2>
     40      <p><%- t('ahelp.s_audio_b') %></p>
    4141    </section>
    4242
    4343    <section class="hb-item">
    44       <h2>© Credit, licentie &amp; "open in"</h2>
    45       <p>Per nummer kun je een <strong>eigenaar/credit</strong> (met © -knop) en een <strong>licentie</strong> instellen — die worden ook in de mp3-metadata geschreven. Met de <strong>open-in</strong>-velden (Spotify / YouTube / SoundCloud) verschijnen knoppen om het nummer op die platforms te openen.</p>
     44      <h2>© <%= t('ahelp.s_credit_h') %></h2>
     45      <p><%- t('ahelp.s_credit_b') %></p>
    4646    </section>
    4747
    4848    <section class="hb-item">
    49       <h2>⬇ Downloads <small>(premium)</small></h2>
    50       <p>Markeer een nummer als <strong>downloadbaar</strong> in Beheer → Audio (⬇-knop). Bezoekers vinden ze op de <strong>/downloads</strong>-pagina en laten hun e-mail achter om het bestand te krijgen (komt op je mailinglijst). Wil je downloads prominent in de feed? Maak een gewone post met slug <code>downloads</code> en pin 'm.</p>
     49      <h2>⬇ <%= t('ahelp.s_downloads_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     50      <p><%- t('ahelp.s_downloads_b') %></p>
    5151    </section>
    5252
    5353    <section class="hb-item">
    54       <h2>📃 Albums &amp; playlists</h2>
    55       <p>Geef nummers hetzelfde <strong>album</strong> + een <strong>positie</strong> om een album te vormen. Playlists maak je in Beheer → <strong>Playlists</strong>. Beide toon je in een post met <code>[[album:Naam]]</code> of <code>[[playlist:id]]</code>.</p>
     54      <h2>📃 <%= t('ahelp.s_albums_h') %></h2>
     55      <p><%- t('ahelp.s_albums_b') %></p>
    5656    </section>
    5757
    5858    <section class="hb-item">
    59       <h2>📅 Agenda / evenementen <small>(premium)</small></h2>
    60       <p>Beheer → <strong>Agenda</strong>. Zet bovenaan <strong>"Agenda tonen op de site"</strong> aan — dan verschijnt de Agenda-knop in de balk en is de agendapagina bereikbaar. Voeg evenementen toe (datum, plaats, locatie, tickets). Bezoekers kunnen zich (los van de nieuwsbrief) aanmelden voor een seintje bij een nieuw evenement.</p>
     59      <h2>📅 <%= t('ahelp.s_agenda_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     60      <p><%- t('ahelp.s_agenda_b') %></p>
    6161    </section>
    6262
    6363    <section class="hb-item">
    64       <h2>📰 Perskit <small>(premium)</small></h2>
    65       <p>Een deelbare perspagina op <strong>/pers</strong>. Bewerk 'm via de <strong>✎ Bewerken</strong>-knop op die pagina zelf (alleen jij ziet die). Stel een korte pers-bio + contact in, en kies <strong>tot 5 nummers</strong> die getoond worden (of laat leeg = automatisch de top-5 meest beluisterd).</p>
     64      <h2>📰 <%= t('ahelp.s_presskit_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     65      <p><%- t('ahelp.s_presskit_b') %></p>
    6666    </section>
    6767
    6868    <section class="hb-item">
    69       <h2>🔗 Cirkels (federatie)</h2>
    70       <p>In Beheer → Instellingen zet je je site op <strong>Cirkel</strong>-modus. Daarna voeg je in Beheer → <strong>Cirkel</strong> andere Klonkt-sites toe; hun publieke posts verschijnen dan in je <strong>/cirkel</strong>-feed. Wat anderen van jóu zien is de titel + de "Samenvatting &amp; Cirkel Preview". Hubs doen niet mee aan cirkels (alleen solo-sites).</p>
     69      <h2>🔗 <%= t('ahelp.s_circles_h') %></h2>
     70      <p><%- t('ahelp.s_circles_b') %></p>
    7171    </section>
    7272
    7373    <section class="hb-item">
    74       <h2>📊 Statistieken <small>(premium)</small></h2>
    75       <p>Beheer → <strong>Statistieken</strong>. Cookievrij gemeten. <strong>Bezoeker-dagen</strong> = unieke bezoekers per dag, opgeteld (géén aantal personen). <strong>Weergaven</strong> = home/feed- en post-loads. Beheerder-bezoeken en bots tellen niet mee. Goed voor trends; absolute aantallen met een korrel zout.</p>
     74      <h2>📊 <%= t('ahelp.s_stats_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     75      <p><%- t('ahelp.s_stats_b') %></p>
    7676    </section>
    7777
    7878    <section class="hb-item">
    79       <h2>✉️ Nieuwsbrief <small>(premium)</small></h2>
    80       <p>Beheer → <strong>Nieuwsbrief</strong>: stel een bericht op en stuur het naar je bevestigde abonnees. Bezoekers melden zich aan via de footer of <strong>/nieuwsbrief</strong>. Versturen vereist dat e-mail (SMTP) is ingesteld.</p>
     79      <h2>✉️ <%= t('ahelp.s_newsletter_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     80      <p><%- t('ahelp.s_newsletter_b') %></p>
    8181    </section>
    8282
    8383    <section class="hb-item">
    84       <h2>🔗 Link-in-bio <small>(premium)</small></h2>
    85       <p>Een Linktree-achtige pagina op <strong>/links</strong> met je profiel-links. De kliks per link zie je terug in Statistieken.</p>
     84      <h2>🔗 <%= t('ahelp.s_linkbio_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     85      <p><%- t('ahelp.s_linkbio_b') %></p>
    8686    </section>
    8787
    8888    <section class="hb-item">
    89       <h2>▶️ Embedbare speler <small>(premium)</small></h2>
    90       <p>Beheer → Audio toont een kopieerbare <code>&lt;iframe&gt;</code>-code (<strong>/embed</strong>) waarmee je je speler op een andere website kunt insluiten.</p>
     89      <h2>▶️ <%= t('ahelp.s_embed_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
     90      <p><%- t('ahelp.s_embed_b') %></p>
    9191    </section>
    9292
    9393    <section class="hb-item">
    94       <h2>🎨 Uiterlijk (thema, foto, accent)</h2>
    95       <p>Beheer → <strong>Uiterlijk</strong>: stel je sitenaam, tagline, profielfoto, accentkleur en kleurpalet in, en de standaard feed-weergave (Tijdlijn of Grid).</p>
     94      <h2>🎨 <%= t('ahelp.s_appearance_h') %></h2>
     95      <p><%- t('ahelp.s_appearance_b') %></p>
    9696    </section>
    9797
    9898    <section class="hb-item">
    99       <h2>👥 Solo- / Hub-modus</h2>
    100       <p>Beheer → <strong>Instellingen</strong>: <em>Solo</em> = één site (jij). <em>Hub</em> = een bedrijfs-/labelsite met meerdere makers, elk een eigen Klonkt onder <code>/user/&lt;naam&gt;</code>. <em>Cirkel</em> = solo + federatie met andere sites.</p>
     99      <h2>👥 <%= t('ahelp.s_tenancy_h') %></h2>
     100      <p><%- t('ahelp.s_tenancy_b') %></p>
    101101    </section>
    102102
    103103    <section class="hb-item">
    104       <h2>⭐ Premium / Patreon</h2>
    105       <p>Premium-functies (Hub, Statistieken, Agenda, Downloads, Perskit, Nieuwsbrief, Link-in-bio, Embed, fan-login) ontgrendel je in Beheer → Instellingen door Patreon te koppelen ($10 lifetime). Updates en de kernapp blijven altijd gratis.</p>
     104      <h2>⭐ <%= t('ahelp.s_premium_h') %></h2>
     105      <p><%- t('ahelp.s_premium_b') %></p>
    106106    </section>
    107107
    108108    <section class="hb-item">
    109       <h2>🔑 Google-login (bezoekers)</h2>
    110       <p>Beheer → Instellingen → <strong>Google</strong>. Optioneel: laat bezoekers/fans met Google inloggen om te reageren. Jij beheert met je wachtwoord — Google geeft nooit beheerrechten. Vul je eigen Google-client-gegevens in (per site).</p>
     109      <h2>🔑 <%= t('ahelp.s_google_h') %></h2>
     110      <p><%- t('ahelp.s_google_b') %></p>
    111111    </section>
    112112
    113113    <section class="hb-item">
    114       <h2>🔄 Updates</h2>
    115       <p>Beheer → <strong>Updates</strong> (alleen god) toont de huidige versie en of er een nieuwere is. Met "Nu bijwerken" haal je de laatste versie binnen.</p>
     114      <h2>🔄 <%= t('ahelp.s_updates_h') %></h2>
     115      <p><%- t('ahelp.s_updates_b') %></p>
    116116    </section>
    117117
    118118  </div>
    119119
    120   <p class="hb-empty" id="hb-empty" hidden>Geen onderwerpen gevonden voor je zoekopdracht.</p>
     120  <p class="hb-empty" id="hb-empty" hidden><%= t('ahelp.empty') %></p>
    121121</div>
    122122
  • src/views/pages/admin-newsletter.ejs

    r929d98d rbbf9d1a  
    77%>
    88<section class="anl">
    9   <h1>Nieuwsbrief</h1>
     9  <h1><%= t('anews.title') %></h1>
    1010
    1111  <% if (msg) { %><div class="anl-msg anl-<%= msgKind %>"><%= msg %></div><% } %>
    1212
    1313  <div class="anl-stats">
    14     <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l">bevestigd</span></div>
    15     <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l">in afwachting</span></div>
    16     <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l">uitgeschreven</span></div>
     14    <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l"><%= t('anews.confirmed') %></span></div>
     15    <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l"><%= t('anews.pending') %></span></div>
     16    <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l"><%= t('anews.unsub') %></span></div>
    1717  </div>
    1818
    1919  <% if (!smtp) { %>
    20     <div class="anl-msg anl-warn">⚠ SMTP is nog niet ingesteld. Aanmeldingen worden wél verzameld, maar versturen kan pas als je SMTP-gegevens invult (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> in <code>.env</code>).</div>
     20    <div class="anl-msg anl-warn"><%= t('anews.smtp_warn_1') %> (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> <%= t('anews.smtp_warn_2') %> <code>.env</code>).</div>
    2121  <% } %>
    2222
    23   <p class="anl-share">Aanmeldlink om te delen: <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>
     23  <p class="anl-share"><%= t('anews.share') %> <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>
    2424
     25  <% var _sendConfirm = t('anews.send_confirm', { n: c.confirmed }); %>
    2526  <form method="POST" action="<%= siteUrlBase %>/admin/newsletter/send" class="anl-form">
    26     <label>Onderwerp
    27       <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="Nieuwe single uit!">
     27    <label><%= t('anews.subject') %>
     28      <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="<%= t('anews.subject_ph') %>">
    2829    </label>
    29     <label>Bericht
    30       <textarea name="body" rows="10" required placeholder="Schrijf je update…"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>
     30    <label><%= t('anews.body') %>
     31      <textarea name="body" rows="10" required placeholder="<%= t('anews.body_ph') %>"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>
    3132    </label>
    32     <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm('Nieuwsbrief versturen naar <%= c.confirmed %> bevestigde abonnee(s)?')">Versturen naar <%= c.confirmed %> abonnee(s)</button>
     33    <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm('<%= _sendConfirm %>')"><%= t('anews.send_btn', { n: c.confirmed }) %></button>
    3334  </form>
    3435
    3536  <% if (hist.length) { %>
    36     <h2 class="anl-h2">Verstuurd</h2>
     37    <h2 class="anl-h2"><%= t('anews.sent_heading') %></h2>
     38    <% var _recipients = t('anews.recipients'); %>
    3739    <ul class="anl-hist">
    3840      <% hist.forEach(function(h){ %>
    39         <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> ontvanger(s) · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>
     41        <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> <%= _recipients %> · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>
    4042      <% }); %>
    4143    </ul>
  • src/views/pages/admin-playlists.ejs

    r929d98d rbbf9d1a  
    1010  <header class="ax-header">
    1111    <div>
    12       <h1>Playlists</h1>
     12      <h1><%= t('apl.title') %></h1>
    1313      <p class="ax-tagline">
    14         Canonieke playlists. Bewerk een playlist hier en de wijzigingen werken
    15         door in álle posts die hem gebruiken via <code>[[playlist:&lt;id&gt;]]</code>.
     14        <%= t('apl.tagline_pre') %> <code>[[playlist:&lt;id&gt;]]</code><%= t('apl.tagline_post') %>
    1615      </p>
    1716    </div>
     
    2120  <div class="ax-actions-bar">
    2221    <button type="button" class="ax-btn ax-btn-primary" id="pl-new-btn">
    23       + Nieuwe playlist
     22      + <%= t('apl.new_playlist') %>
    2423    </button>
    2524  </div>
     
    2928      <div class="ax-empty">
    3029        <div class="ax-empty-icon">📃</div>
    31         <p>Nog geen playlists.</p>
    32         <p class="ax-empty-sub">Maak er een via de knop hierboven, of via de 📃 knop in de post-editor.</p>
     30        <p><%= t('apl.none') %></p>
     31        <p class="ax-empty-sub"><%= t('apl.none_sub') %></p>
    3332      </div>
    3433    </div>
    3534  <% } else { %>
     35    <%
     36      // Pre-fetch loop labels (defensive — keep `t` calls out of the loop body).
     37      var _aplPlaylist = t('apl.pill_playlist');
     38      var _aplAlbum    = t('apl.pill_album');
     39      var _aplTrack    = t('apl.track');
     40      var _aplTracks   = t('apl.tracks');
     41      var _aplCopy     = t('apl.copy_click');
     42      var _aplEdit     = t('apl.edit');
     43      var _aplDelete   = t('apl.delete');
     44    %>
    3645    <ul class="ax-list">
    3746      <% playlists.forEach(function(p) { %>
     
    4453            <div class="ax-track-title">
    4554              <%= p.title %>
    46               <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 playlist' : '💿 album' %></span>
     55              <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 ' + _aplPlaylist : '💿 ' + _aplAlbum %></span>
    4756            </div>
    4857            <div class="ax-track-sub">
    49               <span><%= p.track_count %> <%= p.track_count === 1 ? 'track' : 'tracks' %></span>
     58              <span><%= p.track_count %> <%= p.track_count === 1 ? _aplTrack : _aplTracks %></span>
    5059              <% if (p.artist) { %><span class="ax-track-sep">·</span><span><%= p.artist %></span><% } %>
    5160              <% if (p.year) { %><span class="ax-track-sep">·</span><span><%= p.year %></span><% } %>
    5261            </div>
    53             <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="Klik om te kopiëren">[[playlist:<%= p.id %>]]</code>
     62            <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="<%= _aplCopy %>">[[playlist:<%= p.id %>]]</code>
    5463          </div>
    5564
    5665          <div class="ax-track-actions">
    57             <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="Bewerken" title="Bewerken">✎</button>
    58             <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label="Verwijderen" title="Verwijderen">🗑</button>
     66            <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="<%= _aplEdit %>" title="<%= _aplEdit %>">✎</button>
     67            <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label="<%= _aplDelete %>" title="<%= _aplDelete %>">🗑</button>
    5968          </div>
    6069        </li>
     
    229238        el.classList.add('is-copied');
    230239        const original = el.textContent;
    231         el.textContent = '✓ gekopieerd';
     240        el.textContent = '✓ <%= t('apl.copied') %>';
    232241        setTimeout(() => { el.classList.remove('is-copied'); el.textContent = original; }, 1200);
    233242      } catch (_) { /* fall back to selection */ }
     
    247256      const id = btn.dataset.id;
    248257      const title = btn.dataset.title || id;
    249       if (!confirm(`Playlist "${title}" verwijderen? Posts die deze playlist embedden tonen vanaf nu een placeholder.`)) return;
     258      if (!confirm('<%= t('apl.delete_confirm') %>'.replace('{title}', title))) return;
    250259      try {
    251260        const r = await fetch(`/admin/playlists/api/${encodeURIComponent(id)}/delete`, {
     
    256265        const j = await r.json();
    257266        if (j.ok) location.reload();
    258         else alert('Verwijderen mislukt: ' + (j.error || ''));
     267        else alert('<%= t('apl.delete_failed') %>: ' + (j.error || ''));
    259268      } catch (err) {
    260         alert('Verwijderen mislukt: ' + err.message);
     269        alert('<%= t('apl.delete_failed') %>: ' + err.message);
    261270      }
    262271    });
  • src/views/pages/admin-seo.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-site-edit-page">
    2   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    3   <h1>SEO &amp; vindbaarheid</h1>
    4   <p class="admin-tagline">Geavanceerde SEO van <strong><%= site.title %></strong> — hoe je site in zoekmachines en bij het delen op social media verschijnt.</p>
     2  <p><a href="/admin" class="btn">&larr; <%= t('aseo.back') %></a></p>
     3  <h1><%= t('aseo.title') %></h1>
     4  <p class="admin-tagline"><%= t('aseo.tagline_pre') %> <strong><%= site.title %></strong> <%= t('aseo.tagline_post') %></p>
    55
    66  <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
     
    1010
    1111    <fieldset>
    12       <legend>Indexeren</legend>
     12      <legend><%= t('aseo.index_legend') %></legend>
    1313      <label class="cb">
    1414        <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
    15         <span>Zoekmachines mogen deze site indexeren <small class="form-hint-inline">— uit = <code>noindex</code> + sitemap.xml verborgen</small></span>
     15        <span><%= t('aseo.index_label') %> <small class="form-hint-inline">— <%= t('aseo.index_hint_pre') %> <code>noindex</code> <%= t('aseo.index_hint_post') %></small></span>
    1616      </label>
    1717    </fieldset>
    1818
    1919    <fieldset>
    20       <legend>Titel &amp; omschrijving</legend>
     20      <legend><%= t('aseo.title_legend') %></legend>
    2121      <label>
    22         <span>Titel-sjabloon <small class="form-hint-inline">— gebruik <code>{title}</code> en <code>{site}</code></small></span>
     22        <span><%= t('aseo.title_template') %> <small class="form-hint-inline">— <%= t('aseo.title_template_hint_pre') %> <code>{title}</code> <%= t('aseo.title_template_hint_and') %> <code>{site}</code></small></span>
    2323        <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200" placeholder="{title} — {site}">
    2424      </label>
    2525      <label>
    26         <span>Standaard-omschrijving <small class="form-hint-inline">— meta description / og:description als een pagina er geen heeft</small></span>
    27         <textarea name="default_description" rows="2" maxlength="500" placeholder="Korte omschrijving van je site (max ~160 tekens werkt het best)"><%= site.default_description || '' %></textarea>
     26        <span><%= t('aseo.default_desc') %> <small class="form-hint-inline">— <%= t('aseo.default_desc_hint') %></small></span>
     27        <textarea name="default_description" rows="2" maxlength="500" placeholder="<%= t('aseo.default_desc_ph') %>"><%= site.default_description || '' %></textarea>
    2828      </label>
    2929      <label>
    30         <span>Canonieke basis-URL <small class="form-hint-inline">— de productie-HTTPS-URL, voorkomt dubbele-content-straf</small></span>
     30        <span><%= t('aseo.canonical') %> <small class="form-hint-inline">— <%= t('aseo.canonical_hint') %></small></span>
    3131        <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://jouw-domein.com" maxlength="200">
    3232      </label>
    3333      <label>
    34         <span>Auteur <small class="form-hint-inline">— meta author-tag</small></span>
    35         <input type="text" name="author" value="<%= site.author || '' %>" maxlength="120" placeholder="Jouw naam">
     34        <span><%= t('aseo.author') %> <small class="form-hint-inline">— <%= t('aseo.author_hint') %></small></span>
     35        <input type="text" name="author" value="<%= site.author || '' %>" maxlength="120" placeholder="<%= t('aseo.author_ph') %>">
    3636      </label>
    3737    </fieldset>
    3838
    3939    <fieldset>
    40       <legend>Delen op social media</legend>
     40      <legend><%= t('aseo.social_legend') %></legend>
    4141      <label>
    42         <span>Standaard deel-afbeelding (URL) <small class="form-hint-inline">— og:image / Twitter-card; ~1200×630px</small></span>
     42        <span><%= t('aseo.og_image') %> <small class="form-hint-inline">— <%= t('aseo.og_image_hint') %></small></span>
    4343        <input type="text" name="og_image_default" value="<%= site.og_image_default || '' %>" placeholder="https://jouw-domein.com/share.jpg" maxlength="500">
    4444      </label>
    4545      <label>
    46         <span>Taal-locale <small class="form-hint-inline">— og:locale, bv. <code>nl_NL</code> of <code>en_US</code></small></span>
     46        <span><%= t('aseo.og_locale') %> <small class="form-hint-inline">— <%= t('aseo.og_locale_hint_pre') %> <code>nl_NL</code> <%= t('aseo.og_locale_hint_or') %> <code>en_US</code></small></span>
    4747        <input type="text" name="og_locale" value="<%= site.og_locale || '' %>" placeholder="nl_NL" maxlength="32">
    4848      </label>
    4949      <label>
    50         <span>Twitter / X-handle <small class="form-hint-inline">— met @</small></span>
     50        <span><%= t('aseo.twitter') %> <small class="form-hint-inline">— <%= t('aseo.twitter_hint') %></small></span>
    5151        <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@jouwnaam" maxlength="64">
    5252      </label>
    5353      <label>
    54         <span>Facebook App-ID <small class="form-hint-inline">— fb:app_id (optioneel)</small></span>
     54        <span><%= t('aseo.fb_app') %> <small class="form-hint-inline">— <%= t('aseo.fb_app_hint') %></small></span>
    5555        <input type="text" name="facebook_app_id" value="<%= site.facebook_app_id || '' %>" maxlength="64">
    5656      </label>
     
    5858
    5959    <fieldset>
    60       <legend>Uitgever (JSON-LD / rich results)</legend>
     60      <legend><%= t('aseo.publisher_legend') %></legend>
    6161      <label>
    62         <span>Type</span>
     62        <span><%= t('aseo.type') %></span>
    6363        <select name="schema_type">
    64           <option value="Person" <%= site.schema_type === 'Organization' ? '' : 'selected' %>>Persoon</option>
    65           <option value="Organization" <%= site.schema_type === 'Organization' ? 'selected' : '' %>>Organisatie / bedrijf</option>
     64          <option value="Person" <%= site.schema_type === 'Organization' ? '' : 'selected' %>><%= t('aseo.type_person') %></option>
     65          <option value="Organization" <%= site.schema_type === 'Organization' ? 'selected' : '' %>><%= t('aseo.type_org') %></option>
    6666        </select>
    6767      </label>
    6868      <label>
    69         <span>Naam <small class="form-hint-inline">— valt terug op de site-titel</small></span>
     69        <span><%= t('aseo.publisher_name') %> <small class="form-hint-inline">— <%= t('aseo.publisher_name_hint') %></small></span>
    7070        <input type="text" name="publisher_name" value="<%= site.publisher_name || '' %>" maxlength="200">
    7171      </label>
    7272      <label>
    73         <span>URL</span>
     73        <span><%= t('aseo.publisher_url') %></span>
    7474        <input type="text" name="publisher_url" value="<%= site.publisher_url || '' %>" placeholder="https://jouw-domein.com" maxlength="200">
    7575      </label>
    7676      <label>
    77         <span>Logo (URL)</span>
     77        <span><%= t('aseo.publisher_logo') %></span>
    7878        <input type="text" name="publisher_logo" value="<%= site.publisher_logo || '' %>" placeholder="https://jouw-domein.com/logo.png" maxlength="500">
    7979      </label>
     
    8181
    8282    <fieldset>
    83       <legend>Zoekmachine-verificatie</legend>
     83      <legend><%= t('aseo.verify_legend') %></legend>
    8484      <label>
    85         <span>Google site-verificatie</span>
     85        <span><%= t('aseo.verify_google') %></span>
    8686        <input type="text" name="google_verification" value="<%= site.google_verification || '' %>" maxlength="200">
    8787      </label>
    8888      <label>
    89         <span>Bing <small class="form-hint-inline">— msvalidate.01</small></span>
     89        <span><%= t('aseo.verify_bing') %> <small class="form-hint-inline">— <%= t('aseo.verify_bing_hint') %></small></span>
    9090        <input type="text" name="bing_verification" value="<%= site.bing_verification || '' %>" maxlength="200">
    9191      </label>
    9292      <label>
    93         <span>Pinterest <small class="form-hint-inline">— p:domain_verify</small></span>
     93        <span><%= t('aseo.verify_pinterest') %> <small class="form-hint-inline">— <%= t('aseo.verify_pinterest_hint') %></small></span>
    9494        <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200">
    9595      </label>
    9696      <label>
    97         <span>Yandex</span>
     97        <span><%= t('aseo.verify_yandex') %></span>
    9898        <input type="text" name="yandex_verification" value="<%= site.yandex_verification || '' %>" maxlength="200">
    9999      </label>
    100100    </fieldset>
    101101
    102     <button type="submit" class="btn btn-primary">SEO opslaan</button>
     102    <button type="submit" class="btn btn-primary"><%= t('aseo.save') %></button>
    103103  </form>
    104104</div>
  • src/views/pages/admin-settings.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page">
    2   <h1>Instellingen</h1>
    3   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
     2  <h1><%= t('aset.title') %></h1>
     3  <p><a href="/admin" class="btn">&larr; <%= t('aset.back_admin') %></a></p>
    44
    55  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
     
    77
    88  <section class="set-card">
    9     <h2>Modus</h2>
     9    <h2><%= t('aset.mode') %></h2>
    1010    <p class="set-help">
    11       Bepaalt hoe deze installatie werkt. Wisselen is veilig: er wordt niets verwijderd —
    12       Solo verbergt alleen de multi-onderdelen en toont je primaire site.
     11      <%= t('aset.mode_help') %>
    1312    </p>
    1413
     
    1716        <input type="radio" name="tenancy" value="solo" <%= tenancy === 'solo' ? 'checked' : '' %>>
    1817        <span>
    19           <strong>Solo</strong> — één site (de jouwe).
    20           <small>Geen gebruikers-directory, geen site-wissel. Bezoekers kunnen wel met Google inloggen om te reageren.</small>
     18          <strong><%= t('aset.solo') %></strong> — <%= t('aset.solo_title') %>
     19          <small><%= t('aset.solo_desc') %></small>
    2120        </span>
    2221      </label>
     
    2524        <input type="radio" name="tenancy" value="hub" <%= tenancy === 'hub' ? 'checked' : '' %> <%= _hubLocked ? 'disabled' : '' %>>
    2625        <span>
    27           <strong>Hub</strong> — bedrijfssite met <code>/user/</code>.
    28           <% if (_hubLocked) { %><span style="display:inline-block;font-size:.7rem;font-weight:700;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:.05rem .45rem;margin-left:.3rem;vertical-align:1px;">🔒 premium</span><% } %>
    29           <small>Jij (admin) wijst gebruikers een eigen Klonkt Hub toe. Gesloten cirkel: geen open registratie.<% if (_hubLocked) { %> <em>Vereist premium — koppel Patreon in deze pagina.</em><% } %></small>
     26          <strong><%= t('aset.hub') %></strong> — <%= t('aset.hub_title') %> <code>/user/</code>.
     27          <% if (_hubLocked) { %><span style="display:inline-block;font-size:.7rem;font-weight:700;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:.05rem .45rem;margin-left:.3rem;vertical-align:1px;">🔒 <%= t('aset.premium_badge') %></span><% } %>
     28          <small><%= t('aset.hub_desc') %><% if (_hubLocked) { %> <em><%= t('aset.hub_locked_note') %></em><% } %></small>
    3029        </span>
    3130      </label>
     
    3332        <input type="radio" name="tenancy" value="circle" <%= tenancy === 'circle' ? 'checked' : '' %>>
    3433        <span>
    35           <strong>Cirkels</strong> &mdash; solo + federatie.
    36           <small>E&eacute;n eigen site die de publieke posts van andere Klonkt-sites toont. Asymmetrisch: jij bepaalt wie in jouw cirkel zit.</small>
    37         </span>
    38       </label>
    39       <button type="submit" class="btn btn-primary">Opslaan</button>
     34          <strong><%= t('aset.circle') %></strong> &mdash; <%= t('aset.circle_title') %>
     35          <small><%= t('aset.circle_desc') %></small>
     36        </span>
     37      </label>
     38      <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button>
    4039    </form>
    4140  </section>
     
    4544  <% if (tenancy === 'hub') { %>
    4645  <section class="set-card set-card--mode" style="margin-top:1rem">
    47     <h2>Hub-hoofdpagina</h2>
     46    <h2><%= t('aset.hub_home') %></h2>
    4847    <p class="set-help">
    49       De openbare hub-pagina (<code>/</code>) is generiek — van geen enkele gebruiker.
    50       Hier stel je de naam en intro in. Jouw eigen Klonkt Hub blijft los bestaan onder <code>/user/</code>.
     48      <%= t('aset.hub_home_help_1') %> (<code>/</code>) <%= t('aset.hub_home_help_2') %> <code>/user/</code>.
    5149    </p>
    5250    <form method="post" action="/admin/settings" class="set-form" enctype="multipart/form-data">
    5351      <label class="set-field">
    54         <span>Naam</span>
    55         <input type="text" name="hub_title" maxlength="80" value="<%= hubTitle %>" placeholder="bijv. Studio Noord">
    56       </label>
    57       <label class="set-field">
    58         <span>Tagline</span>
    59         <input type="text" name="hub_tagline" maxlength="120" value="<%= hubTagline %>" placeholder="bijv. Onafhankelijk muzieklabel">
    60       </label>
    61       <label class="set-field">
    62         <span>Intro</span>
    63         <textarea name="hub_intro" maxlength="400" rows="3" placeholder="Korte introtekst onder de titel."><%= hubIntro %></textarea>
    64       </label>
    65       <label class="set-field">
    66         <span>Hero-afbeelding (URL) <small style="font-weight:400;color:var(--ink-muted)">— optioneel; achtergrond van de hero</small></span>
     52        <span><%= t('aset.name') %></span>
     53        <input type="text" name="hub_title" maxlength="80" value="<%= hubTitle %>" placeholder="<%= t('aset.name_ph') %>">
     54      </label>
     55      <label class="set-field">
     56        <span><%= t('aset.tagline') %></span>
     57        <input type="text" name="hub_tagline" maxlength="120" value="<%= hubTagline %>" placeholder="<%= t('aset.tagline_ph') %>">
     58      </label>
     59      <label class="set-field">
     60        <span><%= t('aset.intro') %></span>
     61        <textarea name="hub_intro" maxlength="400" rows="3" placeholder="<%= t('aset.intro_ph') %>"><%= hubIntro %></textarea>
     62      </label>
     63      <label class="set-field">
     64        <span><%= t('aset.hero_image') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_image_hint') %></small></span>
    6765        <input type="text" name="hub_hero_image" maxlength="300" value="<%= hubHeroImage %>" placeholder="/media/... of https://...">
    6866      </label>
    6967      <label class="set-field">
    70         <span>…of upload een afbeelding <small style="font-weight:400;color:var(--ink-muted)">— jpg/png/webp/gif, max 5 MB; vervangt de URL hierboven</small></span>
     68        <span><%= t('aset.hero_upload') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_upload_hint') %></small></span>
    7169        <input type="file" name="hub_hero_file" accept="image/png,image/jpeg,image/webp,image/gif">
    7270      </label>
    7371      <label class="set-field">
    74         <span>Donkere overlay <small style="font-weight:400;color:var(--ink-muted)">— maakt de hero donkerder zodat de tekst leesbaar blijft</small></span>
     72        <span><%= t('aset.hero_overlay') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_overlay_hint') %></small></span>
    7573        <span style="display:flex;align-items:center;gap:0.75rem">
    7674          <input type="range" name="hub_hero_overlay" min="0" max="100" step="5" value="<%= hubHeroOverlay %>" style="flex:1"
     
    8179      <% if (hubHeroImage) { %>
    8280      <div class="set-field">
    83         <span style="font-weight:400;color:var(--ink-muted);font-size:0.9rem">Voorbeeld (met overlay):</span>
     81        <span style="font-weight:400;color:var(--ink-muted);font-size:0.9rem"><%= t('aset.preview_overlay') %></span>
    8482        <div style="position:relative;width:100%;aspect-ratio:16/7;margin-top:0.4rem;border-radius:8px;overflow:hidden;border:1px solid var(--rule);background:var(--paper-2)">
    85           <img src="<%= hubHeroImage %>" alt="Hero-voorbeeld" style="display:block;width:100%;height:100%;object-fit:cover">
     83          <img src="<%= hubHeroImage %>" alt="<%= t('aset.hero_preview_alt') %>" style="display:block;width:100%;height:100%;object-fit:cover">
    8684          <div id="hero-ov-preview" style="position:absolute;inset:0;background:#000;opacity:<%= (hubHeroOverlay/100).toFixed(2) %>;pointer-events:none"></div>
    8785        </div>
    8886      </div>
    8987      <% } %>
    90       <button type="submit" class="btn btn-primary">Opslaan</button>
     88      <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button>
    9189    </form>
    9290  </section>
     
    9593  <% if (tenancy === 'circle') { %>
    9694  <section class="set-card set-card--mode" style="margin-top:1rem">
    97     <h2>🔗 Je cirkel</h2>
     95    <h2>🔗 <%= t('aset.your_circle') %></h2>
    9896    <p class="set-help">
    99       Beheer welke andere Klonkt-sites je in je cirkel toont, en of jouw site in
    100       cirkels van anderen mag verschijnen. Asymmetrisch: jij bepaalt wie je volgt.
     97      <%= t('aset.your_circle_help') %>
    10198    </p>
    10299    <div class="set-actions">
    103       <a href="/admin/circle" class="btn btn-primary">Beheer je cirkel &rarr;</a>
     100      <a href="/admin/circle" class="btn btn-primary"><%= t('aset.manage_circle') %> &rarr;</a>
    104101    </div>
    105102  </section>
     
    110107  <% if (premium && premium.enabled) { %>
    111108  <section class="set-card" style="margin-top:1rem">
    112     <h2>Premium (Patreon)</h2>
    113     <p class="set-help">Ontgrendel de premium-modules (Hub-modus, Statistieken, Fan-login,
    114       Prutter-DMs) met je <strong>$10-lifetime</strong> Patreon-steun. De app en alle updates
    115       blijven gratis.</p>
     109    <h2><%= t('aset.premium') %></h2>
     110    <p class="set-help"><%= t('aset.premium_help_1') %> <strong><%= t('aset.premium_lifetime') %></strong> <%= t('aset.premium_help_2') %></p>
    116111    <% if (premium.premium) { %>
    117       <p class="set-help"><strong>✓ Premium actief.</strong>
    118         <% if (premium.supportCents) { %>Lifetime-steun: $<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p>
    119       <a href="/admin/patreon/disconnect" class="btn">Patreon ontkoppelen</a>
     112      <p class="set-help"><strong>✓ <%= t('aset.premium_active') %></strong>
     113        <% if (premium.supportCents) { %><%= t('aset.lifetime_support') %> $<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p>
     114      <a href="/admin/patreon/disconnect" class="btn"><%= t('aset.patreon_disconnect') %></a>
    120115    <% } else if (premium.connected) { %>
    121       <p class="set-help">Patreon gekoppeld, maar nog geen $10 lifetime<% if (premium.supportCents) { %>
    122         (nu $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. Steun de campagne en koppel opnieuw.</p>
    123       <a href="/admin/patreon/connect" class="btn btn-primary">Opnieuw koppelen</a>
     116      <p class="set-help"><%= t('aset.patreon_no_lifetime') %><% if (premium.supportCents) { %>
     117        (<%= t('aset.now') %> $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. <%= t('aset.patreon_support_again') %></p>
     118      <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_reconnect') %></a>
    124119    <% } else { %>
    125       <p class="set-help">Nog niet gekoppeld.</p>
    126       <a href="/admin/patreon/connect" class="btn btn-primary">Koppel Patreon</a>
     120      <p class="set-help"><%= t('aset.patreon_not_connected') %></p>
     121      <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_connect') %></a>
    127122    <% } %>
    128123  </section>
     
    131126  <%# Google-login staat op een eigen Beheer-pagina (compacte link hier). %>
    132127  <section class="set-card" style="margin-top:1rem">
    133     <h2>Google-login (bezoekers)</h2>
    134     <p class="set-help">Laat bezoekers met Google inloggen om te reageren — met je eigen Google Cloud-client.
    135       <% if (google && google.configured) { %><strong>Status: ingesteld&nbsp;✓</strong><% } else { %>Nog niet ingesteld.<% } %></p>
    136     <div class="set-actions"><a href="/admin/settings/google" class="btn btn-primary">Google-login instellen &rarr;</a></div>
     128    <h2><%= t('aset.google_login') %></h2>
     129    <p class="set-help"><%= t('aset.google_login_help') %>
     130      <% if (google && google.configured) { %><strong><%= t('aset.status_set') %>&nbsp;✓</strong><% } else { %><%= t('aset.not_set_yet') %><% } %></p>
     131    <div class="set-actions"><a href="/admin/settings/google" class="btn btn-primary"><%= t('aset.google_login_setup') %> &rarr;</a></div>
    137132  </section>
    138133
     
    140135  <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
    141136  <section class="set-card" style="margin-top:1rem">
    142     <h2>Nieuwsbrief</h2>
    143     <p class="set-help">Toon een <strong>aanmeldveld in de footer</strong> van je site zodat bezoekers zich op elke pagina kunnen inschrijven. (De volledige aanmeldpagina blijft op <code><%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief</code>.)</p>
     137    <h2><%= t('aset.newsletter') %></h2>
     138    <p class="set-help"><%= t('aset.newsletter_help_1') %> <strong><%= t('aset.newsletter_footer_field') %></strong> <%= t('aset.newsletter_help_2') %> <code><%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief</code>.)</p>
    144139    <form method="post" action="/admin/settings/footer" class="set-form">
    145140      <label class="set-toggle" style="display:flex;align-items:center;gap:.6rem;cursor:pointer">
    146141        <input type="checkbox" name="footer_newsletter" value="1" <%= footerNewsletter ? 'checked' : '' %>>
    147         <span>Aanmeldveld in de footer tonen</span>
    148       </label>
    149       <div class="set-actions"><button type="submit" class="btn btn-primary">Opslaan</button></div>
     142        <span><%= t('aset.newsletter_show_footer') %></span>
     143      </label>
     144      <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div>
    150145    </form>
    151146  </section>
     
    154149  <%# E-mail / SMTP — nodig voor nieuwsbrief versturen, notify-mails en wachtwoord-reset. %>
    155150  <section class="set-card" style="margin-top:1rem">
    156     <h2>E-mail (SMTP)</h2>
    157     <p class="set-help">Nodig om de <strong>nieuwsbrief te versturen</strong>, <strong>show-notify</strong>-mails te sturen en wachtwoord-reset per mail te laten werken. Vul de gegevens van je mailprovider in (bv. je hostingmail, Gmail-app-wachtwoord, Brevo, Mailgun…).
    158       <% if (smtp && smtp.configured) { %><br><strong>Status: ingesteld&nbsp;✓</strong><% if (smtp.fromEnv) { %> <small>(via .env)</small><% } %><% } else { %><br>Nog niet ingesteld — versturen werkt nog niet.<% } %></p>
     151    <h2><%= t('aset.email_smtp') %></h2>
     152    <p class="set-help"><%= t('aset.smtp_help_1') %> <strong><%= t('aset.smtp_help_newsletter') %></strong><%= t('aset.smtp_help_2') %> <strong><%= t('aset.smtp_help_notify') %></strong><%= t('aset.smtp_help_3') %>
     153      <% if (smtp && smtp.configured) { %><br><strong><%= t('aset.status_set') %>&nbsp;✓</strong><% if (smtp.fromEnv) { %> <small>(<%= t('aset.via_env') %>)</small><% } %><% } else { %><br><%= t('aset.smtp_not_set') %><% } %></p>
    159154
    160155    <form method="post" action="/admin/settings/smtp" class="set-form">
    161       <label><span>SMTP-host</span>
     156      <label><span><%= t('aset.smtp_host') %></span>
    162157        <input type="text" name="smtp_host" value="<%= smtp ? smtp.host : '' %>" placeholder="smtp.jouwprovider.nl" autocomplete="off">
    163158      </label>
    164       <label><span>Poort <small style="font-weight:400">— 587 (STARTTLS) of 465 (SSL)</small></span>
     159      <label><span><%= t('aset.smtp_port') %> <small style="font-weight:400">— <%= t('aset.smtp_port_hint') %></small></span>
    165160        <input type="text" name="smtp_port" value="<%= (smtp && smtp.port) ? smtp.port : '' %>" placeholder="587" autocomplete="off" inputmode="numeric">
    166161      </label>
    167       <label><span>Gebruikersnaam</span>
     162      <label><span><%= t('aset.smtp_user') %></span>
    168163        <input type="text" name="smtp_user" value="<%= smtp ? smtp.user : '' %>" placeholder="jij@jouwprovider.nl" autocomplete="off">
    169164      </label>
    170       <label><span>Wachtwoord <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— ingesteld; leeg laten = ongewijzigd</small><% } %></span>
    171         <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? '•••••••• (ingesteld)' : 'app-wachtwoord' %>" autocomplete="off">
    172       </label>
    173       <label><span>Afzender <small style="font-weight:400">— optioneel; standaard = gebruikersnaam</small></span>
    174         <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder="Jouw Naam &lt;jij@jouwprovider.nl&gt;" autocomplete="off">
     165      <label><span><%= t('aset.smtp_pass') %> <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— <%= t('aset.smtp_pass_set_hint') %></small><% } %></span>
     166        <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? t('aset.smtp_pass_ph_set') : t('aset.smtp_pass_ph') %>" autocomplete="off">
     167      </label>
     168      <label><span><%= t('aset.smtp_from') %> <small style="font-weight:400">— <%= t('aset.smtp_from_hint') %></small></span>
     169        <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder="<%= t('aset.smtp_from_ph') %>" autocomplete="off">
    175170      </label>
    176171      <div class="set-actions">
    177         <button type="submit" class="btn btn-primary">SMTP opslaan</button>
     172        <button type="submit" class="btn btn-primary"><%= t('aset.smtp_save') %></button>
    178173        <% if (smtp && (smtp.host || smtp.passSet)) { %>
    179           <button type="submit" name="clear" value="1" class="btn" formnovalidate>Wissen</button>
     174          <button type="submit" name="clear" value="1" class="btn" formnovalidate><%= t('aset.clear') %></button>
    180175        <% } %>
    181176      </div>
     
    184179    <% if (smtp && smtp.configured) { %>
    185180      <form method="post" action="/admin/settings/smtp/test" class="set-form" style="margin-top:.8rem">
    186         <label><span>Testmail sturen naar</span>
     181        <label><span><%= t('aset.test_mail_to') %></span>
    187182          <input type="email" name="to" placeholder="jij@email.nl" autocomplete="off">
    188183        </label>
    189         <div class="set-actions"><button type="submit" class="btn">✉ Testmail sturen</button></div>
     184        <div class="set-actions"><button type="submit" class="btn">✉ <%= t('aset.send_test_mail') %></button></div>
    190185      </form>
    191186    <% } %>
  • src/views/pages/admin-shows.ejs

    r929d98d rbbf9d1a  
    66%>
    77<section class="ash">
    8   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    9   <h1>Agenda</h1>
     8  <p><a href="/admin" class="btn">&larr; <%= t('ashow.back_admin') %></a></p>
     9  <h1><%= t('ashow.title') %></h1>
    1010
    1111  <% if (msg) { %><div class="ash-msg ash-<%= (typeof msgKind!=='undefined')?msgKind:'' %>"><%= msg %></div><% } %>
     
    1515    <label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600">
    1616      <input type="checkbox" name="enabled" value="1" <%= agOn ? 'checked' : '' %> onchange="this.form.submit()" style="width:18px;height:18px;accent-color:var(--accent)">
    17       <span>Agenda tonen op de site <small style="font-weight:400;opacity:.7">(Agenda-knop in de balk + de agendapagina)</small></span>
     17      <span><%= t('ashow.show_toggle') %> <small style="font-weight:400;opacity:.7"><%= t('ashow.show_toggle_hint') %></small></span>
    1818    </label>
    19     <noscript><button type="submit" class="btn">Opslaan</button></noscript>
     19    <noscript><button type="submit" class="btn"><%= t('ashow.save') %></button></noscript>
    2020  </form>
    21   <% if (!agOn) { %><div class="ash-msg">De agenda staat nu <strong>uit</strong> — bezoekers zien geen Agenda-knop en de agendapagina is niet bereikbaar. Zet 'm aan om je evenementen te tonen.</div><% } %>
     21  <% if (!agOn) { %><div class="ash-msg"><%= t('ashow.off_notice_1') %> <strong><%= t('ashow.off') %></strong> <%= t('ashow.off_notice_2') %></div><% } %>
    2222
    23   <p class="ash-note"><strong><%= nc %></strong> abonnee(s) voor evenement-aankondigingen.
    24     <% if (!smtp) { %><br>⚠ SMTP niet ingesteld — evenementen worden opgeslagen, maar notify-mails kunnen pas verstuurd worden zodra je SMTP invult.<% } %></p>
     23  <p class="ash-note"><strong><%= nc %></strong> <%= t('ashow.subscribers') %>
     24    <% if (!smtp) { %><br><%= t('ashow.smtp_warn') %><% } %></p>
    2525
    2626  <form method="POST" action="<%= siteUrlBase %>/admin/shows" class="ash-form">
    2727    <div class="ash-grid">
    28       <label>Datum<input type="date" name="date" required></label>
    29       <label>Tijd (optioneel)<input type="time" name="time"></label>
    30       <label>Plaats<input type="text" name="city" required placeholder="Amsterdam"></label>
    31       <label>Land (optioneel)<input type="text" name="country" placeholder="NL"></label>
    32       <label class="ash-wide">Locatie/zaal (optioneel)<input type="text" name="venue" placeholder="Paradiso"></label>
    33       <label class="ash-wide">Ticket-URL (optioneel)<input type="url" name="ticket_url" placeholder="https://..."></label>
    34       <label class="ash-wide">Notitie (optioneel)<input type="text" name="notes" placeholder="Support: ..."></label>
     28      <label><%= t('ashow.f_date') %><input type="date" name="date" required></label>
     29      <label><%= t('ashow.f_time') %><input type="time" name="time"></label>
     30      <label><%= t('ashow.f_city') %><input type="text" name="city" required placeholder="Amsterdam"></label>
     31      <label><%= t('ashow.f_country') %><input type="text" name="country" placeholder="NL"></label>
     32      <label class="ash-wide"><%= t('ashow.f_venue') %><input type="text" name="venue" placeholder="Paradiso"></label>
     33      <label class="ash-wide"><%= t('ashow.f_ticket') %><input type="url" name="ticket_url" placeholder="https://..."></label>
     34      <label class="ash-wide"><%= t('ashow.f_notes') %><input type="text" name="notes" placeholder="<%= t('ashow.f_notes_ph') %>"></label>
    3535    </div>
    36     <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> Abonnees per e-mail op de hoogte brengen<%= smtp ? '' : ' (SMTP vereist)' %></label>
    37     <button type="submit" class="ash-add">+ Evenement toevoegen</button>
     36    <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> <%= t('ashow.notify_label') %><%= smtp ? '' : ' ' + t('ashow.smtp_required') %></label>
     37    <button type="submit" class="ash-add"><%= t('ashow.add_event') %></button>
    3838  </form>
    3939
    4040  <% if (ss.length) { %>
     41    <% var _delConfirm = t('ashow.del_confirm'); %>
    4142    <ul class="ash-list">
    4243      <% ss.forEach(function(s){ %>
     
    4445          <span class="ash-when"><%= s.date %><% if (s.time) { %> <%= s.time %><% } %></span>
    4546          <span class="ash-where"><%= s.city %><% if (s.venue) { %> — <%= s.venue %><% } %></span>
    46           <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" onsubmit="return confirm('Evenement verwijderen??')" style="margin:0">
     47          <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" onsubmit="return confirm('<%= _delConfirm %>')" style="margin:0">
    4748            <button type="submit" class="ash-del">🗑</button>
    4849          </form>
     
    5152    </ul>
    5253  <% } else { %>
    53     <p class="ash-empty">Nog geen evenementen.</p>
     54    <p class="ash-empty"><%= t('ashow.empty') %></p>
    5455  <% } %>
    5556</section>
  • src/views/pages/admin-site-edit.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-site-edit-page">
    2   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    3   <h1><%= isNew ? 'Nieuwe site' : 'Uiterlijk: ' + site.title %></h1>
     2  <p><a href="/admin" class="btn">&larr; <%= t('asite.back_admin') %></a></p>
     3  <h1><%= isNew ? t('asite.title_new') : t('asite.title_edit') + ': ' + site.title %></h1>
    44
    55  <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
     
    99
    1010    <fieldset>
    11       <legend>Identiteit</legend>
    12       <label>
    13         <span>Slug (URL) <% if (!isNew) { %><em>(vast)</em><% } %></span>
     11      <legend><%= t('asite.identity') %></legend>
     12      <label>
     13        <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span>
    1414        <span class="slug-url">
    1515          <span class="slug-url-host" id="slug-host"
    1616                data-prefix="<%= (typeof tenancy !== 'undefined' && tenancy === 'hub') ? '/user/' : '/' %>">website.com/</span><input
    1717                type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
    18                 pattern="[a-z0-9_-]{2,40}" placeholder="jouwslug"
     18                pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
    1919                autocapitalize="none" autocorrect="off" spellcheck="false">
    2020        </span>
    2121      </label>
    2222      <label>
    23         <span>Titel <small class="form-hint-inline">— getoond in de kop en als weergavenaam</small></span>
     23        <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span>
    2424        <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
    2525      </label>
     
    2828      <% if (user && user.role === 'god') { %>
    2929      <label>
    30         <span>Eigenaar <small class="form-hint-inline">— wie deze Klonkt zelf mag beheren</small></span>
     30        <span><%= t('asite.owner') %> <small class="form-hint-inline"><%= t('asite.owner_hint') %></small></span>
     31        <% var _godSuffix = t('asite.owner_god_suffix'); %>
    3132        <select name="owner_id">
    3233          <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
    33             <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? ' (god)' : '' %></option>
     34            <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? _godSuffix : '' %></option>
    3435          <% }); %>
    3536        </select>
     
    3738      <% } %>
    3839      <label>
    39         <span>Tagline <small class="form-hint-inline">— korte oneliner</small></span>
     40        <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
    4041        <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
    4142      </label>
    4243      <label>
    43         <span>Bio / omschrijving <small class="form-hint-inline">— getoond in de profielkop en gebruikt voor SEO</small></span>
     44        <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
    4445        <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
    4546      </label>
    4647      <label>
    47         <span>Profielfoto</span>
     48        <span><%= t('asite.profile_photo') %></span>
    4849        <div class="photo-picker" id="photo-picker">
    4950          <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
     
    5455          </div>
    5556          <div class="photo-controls">
    56             <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="/media/avatars/foo.jpg of https://…">
     57            <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
    5758            <div class="photo-actions">
    58               <button type="button" class="btn" id="photo-upload-trigger">📷 Uploaden</button>
    59               <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>>Verwijder</button>
     59              <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button>
     60              <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button>
    6061              <small id="photo-status" class="form-hint photo-status"></small>
    6162            </div>
     
    6566      </label>
    6667      <label>
    67         <span>Taal (ISO-code)</span>
     68        <span><%= t('asite.language') %></span>
    6869        <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
    6970      </label>
    7071      <label class="cb">
    7172        <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
    72         <span>Profielkop tonen onder de navigatie</span>
     73        <span><%= t('asite.profile_enabled') %></span>
    7374      </label>
    7475    </fieldset>
    7576
    7677    <fieldset>
    77       <legend>Vormgeving</legend>
     78      <legend><%= t('asite.appearance') %></legend>
    7879
    7980      <%# ── Curated accent picker ───────────────────────────────────────
     
    8283          still what's stored in the DB — we just constrain WHICH hex. %>
    8384      <fieldset class="accent-grid">
    84         <legend>Accentkleur</legend>
     85        <legend><%= t('asite.accent_color') %></legend>
    8586        <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
    8687        <% accents.forEach(function(a) { %>
     
    99100          visitors with a localStorage override, still win over this default. %>
    100101      <fieldset class="theme-default">
    101         <legend>Standaardthema voor nieuwe bezoekers</legend>
     102        <legend><%= t('asite.theme_default') %></legend>
    102103        <% const td = site.theme_override || ''; %>
    103104        <label class="cb">
    104105          <input type="radio" name="theme_override" value=""      <%= td === ''      ? 'checked' : '' %>>
    105           <span>Auto (volg device-voorkeur)</span>
     106          <span><%= t('asite.theme_auto') %></span>
    106107        </label>
    107108        <label class="cb">
    108109          <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
    109           <span>Licht</span>
     110          <span><%= t('asite.theme_light') %></span>
    110111        </label>
    111112        <label class="cb">
    112113          <input type="radio" name="theme_override" value="dark"  <%= td === 'dark'  ? 'checked' : '' %>>
    113           <span>Donker</span>
     114          <span><%= t('asite.theme_dark') %></span>
    114115        </label>
    115116      </fieldset>
    116117
    117118      <fieldset class="palette-grid">
    118         <legend>Palet</legend>
     119        <legend><%= t('asite.palette') %></legend>
    119120        <% palettes.forEach(function(p) { %>
    120121          <label class="palette-swatch">
     
    137138
    138139    <fieldset>
    139       <legend>Gedrag</legend>
     140      <legend><%= t('asite.behavior') %></legend>
    140141      <label class="cb">
    141142        <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
    142         <span>Publieke site (uitvinken voor een besloten kring)</span>
     143        <span><%= t('asite.is_public') %></span>
    143144      </label>
    144145      <label class="cb">
    145146        <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
    146         <span>Zoekmachines mogen indexeren (sitemap.xml is verborgen indien uit)</span>
     147        <span><%= t('asite.robots_index') %></span>
    147148      </label>
    148149      <label class="cb">
    149150        <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>
    150         <span>Inloggen vereist om te reageren</span>
    151       </label>
    152       <label>
    153         <span>Reactie-moderatie</span>
     151        <span><%= t('asite.require_login_comment') %></span>
     152      </label>
     153      <label>
     154        <span><%= t('asite.comment_moderation') %></span>
    154155        <select name="comments_moderation_mode">
    155           <option value="trust"    <%= (site.comments_moderation_mode || 'trust') === 'trust'    ? 'selected' : '' %>>Vertrouwen (automatisch goedkeuren)</option>
    156           <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>>Modereren (eerst in de wachtrij)</option>
     156          <option value="trust"    <%= (site.comments_moderation_mode || 'trust') === 'trust'    ? 'selected' : '' %>><%= t('asite.moderation_trust') %></option>
     157          <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>><%= t('asite.moderation_moderate') %></option>
    157158        </select>
    158159      </label>
    159160      <label class="cb">
    160161        <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
    161         <span>Audiospeler + embeds inschakelen</span>
     162        <span><%= t('asite.enable_audio') %></span>
    162163      </label>
    163164      <%# Prutter is een Hub-only premium-functie — toggle alleen tonen in Hub-modus. %>
     
    165166      <label class="cb">
    166167        <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>>
    167         <span>Prutter (DM&rsquo;s tussen leden) inschakelen <small class="form-hint-inline">— Hub-functie, premium</small></span>
     168        <span><%= t('asite.enable_prutter') %> <small class="form-hint-inline"><%= t('asite.enable_prutter_hint') %></small></span>
    168169      </label>
    169170      <% } %>
     
    171172
    172173    <fieldset class="profile-links-fieldset">
    173       <legend>Social / streaming-links</legend>
    174       <p class="form-hint">Getoond als merk-iconen op de profielkop. Voeg er zoveel toe als je wilt.</p>
     174      <legend><%= t('asite.links') %></legend>
     175      <p class="form-hint"><%= t('asite.links_hint') %></p>
    175176
    176177      <div id="profile-links-rows">
     
    200201      </template>
    201202
    202       <button type="button" id="profile-link-add" class="btn">+ Link toevoegen</button>
     203      <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button>
    203204    </fieldset>
    204205
    205206    <fieldset>
    206       <legend>Feed-weergave</legend>
    207       <label>
    208         <span>Standaardweergave voor de homepage</span>
     207      <legend><%= t('asite.feed_view') %></legend>
     208      <label>
     209        <span><%= t('asite.feed_default') %></span>
    209210        <select name="feed_view_default">
    210           <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>>Tijdlijn (chronologische lijst)</option>
    211           <option value="grid"     <%= site.feed_view_default === 'grid' ? 'selected' : '' %>>Grid (kaarten)</option>
     211          <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_timeline') %></option>
     212          <option value="grid"     <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
    212213        </select>
    213214      </label>
    214215      <label class="cb">
    215216        <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
    216         <span>Tijdlijn ↔ grid-schakelaar boven de feed tonen</span>
     217        <span><%= t('asite.feed_switch') %></span>
    217218      </label>
    218219      <label class="cb">
    219220        <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
    220         <span>Zoekknop in de navigatie tonen</span>
     221        <span><%= t('asite.show_search') %></span>
    221222      </label>
    222223      <label class="cb">
    223224        <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
    224         <span>Archief-link in de navigatie tonen</span>
     225        <span><%= t('asite.show_archive') %></span>
    225226      </label>
    226227    </fieldset>
    227228
    228229    <fieldset class="seo-pointer">
    229       <legend>SEO &amp; social</legend>
    230       <p>Titel-sjabloon, canonical, deel-afbeelding, verificatie-metas en meer staan
    231         nu op een eigen pagina: <a href="/admin/seo">🔎 SEO &amp; vindbaarheid</a>.</p>
     230      <legend><%= t('asite.seo') %></legend>
     231      <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
    232232    </fieldset>
    233233
    234234    <fieldset>
    235       <legend>Eigen CSS &amp; HTML <em>(optioneel)</em></legend>
    236       <label>
    237         <span>Eigen CSS (geïnjecteerd als &lt;style&gt; in &lt;head&gt;)</span>
     235      <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
     236      <label>
     237        <span><%= t('asite.custom_css') %></span>
    238238        <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
    239239      </label>
    240240      <label>
    241         <span>Eigen &lt;head&gt;-HTML (analytics, extra meta&rsquo;s)</span>
     241        <span><%= t('asite.custom_head') %></span>
    242242        <textarea name="custom_head_html" rows="3" placeholder="<meta name=&quot;...&quot;>"><%= site.custom_head_html || '' %></textarea>
    243243      </label>
    244244      <label>
    245         <span>Eigen footer-HTML</span>
     245        <span><%= t('asite.custom_foot') %></span>
    246246        <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
    247247      </label>
    248248    </fieldset>
    249249
    250     <button type="submit" class="btn btn-primary"><%= isNew ? 'Site aanmaken' : 'Wijzigingen opslaan' %></button>
     250    <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
    251251  </form>
    252252</div>
  • src/views/pages/admin-sites.ejs

    r929d98d rbbf9d1a  
    11<div class="container ax-page">
    22
    3   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
     3  <p><a href="/admin" class="btn">&larr; <%= t('asit2.back') %></a></p>
    44  <header class="ax-header">
    55    <div>
    6       <h1>Sites</h1>
    7       <p class="ax-tagline">Beheer alle sites op deze installatie.</p>
     6      <h1><%= t('asit2.title') %></h1>
     7      <p class="ax-tagline"><%= t('asit2.tagline') %></p>
    88    </div>
    99  </header>
     
    1313
    1414  <div class="ax-actions-bar">
    15     <a href="/admin/sites/new" class="ax-btn ax-btn-primary">+ Nieuwe site</a>
     15    <a href="/admin/sites/new" class="ax-btn ax-btn-primary">+ <%= t('asit2.new_site') %></a>
    1616  </div>
    1717
     
    2020      <div class="ax-empty">
    2121        <div class="ax-empty-icon">🌐</div>
    22         <p>Nog geen sites.</p>
    23         <p class="ax-empty-sub">Maak er een via de knop hierboven.</p>
     22        <p><%= t('asit2.empty') %></p>
     23        <p class="ax-empty-sub"><%= t('asit2.empty_sub') %></p>
    2424      </div>
    2525    </div>
    2626  <% } else { %>
     27    <%
     28      var _s_primary    = t('asit2.pill_primary');
     29      var _s_primary_t  = t('asit2.pill_primary_title');
     30      var _s_public     = t('asit2.pill_public');
     31      var _s_public_t   = t('asit2.pill_public_title');
     32      var _s_private    = t('asit2.pill_private');
     33      var _s_private_t  = t('asit2.pill_private_title');
     34      var _s_noindex    = t('asit2.pill_noindex');
     35      var _s_noindex_t  = t('asit2.pill_noindex_title');
     36      var _s_by         = t('asit2.by');
     37      var _s_t_posts    = t('asit2.t_posts');
     38      var _s_l_posts    = t('asit2.l_posts');
     39      var _s_t_created  = t('asit2.t_created');
     40      var _s_l_created  = t('asit2.l_created');
     41      var _s_makeprim   = t('asit2.make_primary');
     42      var _s_makeprim_t = t('asit2.make_primary_title');
     43      var _s_makeprim_c = t('asit2.make_primary_confirm');
     44      var _s_edit       = t('asit2.edit');
     45      var _s_del        = t('asit2.delete');
     46      var _s_del_c      = t('asit2.delete_confirm');
     47    %>
    2748    <ul class="ax-list">
    2849      <% sites.forEach(function(s) { %>
     
    3758                <a href="/user/<%= s.slug %>"><%= s.title %></a>
    3859                <% if (s.is_primary) { %>
    39                   <span class="ax-pill ax-pill-primary" title="Hoofd-/labelsite van deze installatie">★ primair</span>
     60                  <span class="ax-pill ax-pill-primary" title="<%= _s_primary_t %>">★ <%= _s_primary %></span>
    4061                <% } %>
    4162                <% if (s.is_public) { %>
    42                   <span class="ax-pill ax-pill-ok" title="Publiek zichtbaar">✓ public</span>
     63                  <span class="ax-pill ax-pill-ok" title="<%= _s_public_t %>">✓ <%= _s_public %></span>
    4364                <% } else { %>
    44                   <span class="ax-pill" title="Niet publiek">🔒 private</span>
     65                  <span class="ax-pill" title="<%= _s_private_t %>">🔒 <%= _s_private %></span>
    4566                <% } %>
    4667                <% if (!s.robots_index) { %>
    47                   <span class="ax-pill" title="Niet geïndexeerd door zoekmachines">🚫🤖 noindex</span>
     68                  <span class="ax-pill" title="<%= _s_noindex_t %>">🚫🤖 <%= _s_noindex %></span>
    4869                <% } %>
    4970              </div>
     
    5273                <% if (s.owner_username) { %>
    5374                  <span class="ax-track-sep">·</span>
    54                   <span>door <%= s.owner_username %></span>
     75                  <span><%= _s_by %> <%= s.owner_username %></span>
    5576                <% } %>
    5677              </div>
     
    5980
    6081          <div class="ax-site-stats">
    61             <span class="ax-stat" title="Aantal posts">
     82            <span class="ax-stat" title="<%= _s_t_posts %>">
    6283              <span class="ax-stat-num"><%= s.post_count %></span>
    63               <span class="ax-stat-label">posts</span>
     84              <span class="ax-stat-label"><%= _s_l_posts %></span>
    6485            </span>
    65             <span class="ax-stat" title="Aangemaakt op">
     86            <span class="ax-stat" title="<%= _s_t_created %>">
    6687              <span class="ax-stat-num ax-stat-date"><%= formatDate(s.created_at) %></span>
    67               <span class="ax-stat-label">created</span>
     88              <span class="ax-stat-label"><%= _s_l_created %></span>
    6889            </span>
    6990          </div>
     
    7293            <% if (!s.is_primary && (typeof canMutate === 'undefined' || canMutate)) { %>
    7394              <form method="post" action="/admin/sites/<%= s.slug %>/make-primary" style="margin:0;display:inline"
    74                     onsubmit="return confirm('Deze site instellen als de primaire/hoofd-site?')">
    75                 <button type="submit" class="ax-icon-btn" aria-label="Maak primair" title="Maak primaire/hoofd-site">★</button>
     95                    onsubmit="return confirm('<%= _s_makeprim_c %>')">
     96                <button type="submit" class="ax-icon-btn" aria-label="<%= _s_makeprim %>" title="<%= _s_makeprim_t %>">★</button>
    7697              </form>
    7798            <% } %>
    78             <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="Bewerken" title="Bewerken">✎</a>
     99            <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="<%= _s_edit %>" title="<%= _s_edit %>">✎</a>
    79100            <form method="post" action="/admin/sites/<%= s.slug %>/delete" class="ax-delete-form"
    80                   onsubmit="return confirm('Site verwijderen? Lukt alleen als er geen posts zijn.')">
    81               <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
     101                  onsubmit="return confirm('<%= _s_del_c %>')">
     102              <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _s_del %>" title="<%= _s_del %>">🗑</button>
    82103            </form>
    83104          </div>
  • src/views/pages/admin-stats.ejs

    r929d98d rbbf9d1a  
    11<div class="container stats-page">
    22  <p><a href="/admin" class="btn">&larr; Beheer</a></p>
    3   <h1>Statistieken</h1>
    4   <p class="stats-note">Cookievrij gemeten — geen tracking-cookies, geen toestemmingsbanner.
    5     Bezoekers worden per dag geteld via een dagelijks roterende, anonieme hash (IP/browser worden
    6     niet bewaard). Je eigen beheerder-bezoeken en bekende bots/crawlers tellen niet mee.</p>
     3  <h1><%= t('astat.title') %></h1>
     4  <p class="stats-note"><%= t('astat.intro') %></p>
    75
    86  <% var s = stats; var _max = Math.max(1, ...s.series.map(function(d){ return d.pageviews; })); %>
    97
    108  <div class="stats-cards">
    11     <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label">Bezoeker-dagen (<%= s.days %>d)</div></div>
    12     <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label">Weergaven (<%= s.days %>d)</div></div>
    13     <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label">Plays (totaal)</div></div>
    14     <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label">Post-weergaven (totaal)</div></div>
     9    <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label"><%= t('astat.visitor_days', { n: s.days }) %></div></div>
     10    <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label"><%= t('astat.pageviews_days', { n: s.days }) %></div></div>
     11    <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label"><%= t('astat.plays_total') %></div></div>
     12    <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label"><%= t('astat.postviews_total') %></div></div>
    1513  </div>
    1614
    1715  <% if (typeof s.allTime !== 'undefined') { %>
    18     <p class="stats-note">All-time: <strong><%= s.allTime.pageviews %></strong> weergaven · <strong><%= s.allTime.visitorDays %></strong> bezoeker-dagen.</p>
     16    <p class="stats-note"><%= t('astat.alltime_pre') %> <strong><%= s.allTime.pageviews %></strong> <%= t('astat.alltime_mid') %> · <strong><%= s.allTime.visitorDays %></strong> <%= t('astat.alltime_post') %></p>
    1917  <% } %>
    2018
    2119  <details class="stats-help">
    22     <summary>Wat betekenen deze cijfers?</summary>
     20    <summary><%= t('astat.help_summary') %></summary>
    2321    <ul>
    24       <li><strong>Bezoeker-dagen</strong> — het aantal unieke bezoekers <em>per dag, bij elkaar opgeteld</em>. Eén persoon die 5 dagen langskomt = 5 bezoeker-dagen. Cookieloos kan er niet over dagen heen geteld worden, dus dit is géén aantal unieke personen — het echte aantal mensen ligt (vaak fors) lager.</li>
    25       <li><strong>Weergaven</strong> — hoe vaak de home/feed of een post geladen is (ook bij klikken binnen de site). Andere pagina's (agenda, downloads, links) tellen hier niet in mee.</li>
    26       <li><strong>Plays</strong> — totaal aantal keren dat een track is gestart.</li>
    27       <li><strong>Post-weergaven</strong> — totaal over alle posts samen.</li>
     22      <li><strong><%= t('astat.help_vd_term') %></strong> — <%= t('astat.help_vd_a') %> <em><%= t('astat.help_vd_em') %></em><%= t('astat.help_vd_b') %></li>
     23      <li><strong><%= t('astat.help_pv_term') %></strong> — <%= t('astat.help_pv') %></li>
     24      <li><strong><%= t('astat.help_plays_term') %></strong> — <%= t('astat.help_plays') %></li>
     25      <li><strong><%= t('astat.help_postviews_term') %></strong> — <%= t('astat.help_postviews') %></li>
    2826    </ul>
    29     <p>Beheerder-bezoeken en bekende bots/crawlers worden overgeslagen. Het ruwe IP wordt nooit bewaard. Goed voor trends; neem absolute aantallen met een korrel zout.</p>
     27    <p><%= t('astat.help_footer') %></p>
    3028  </details>
    3129
    3230  <div class="stats-period">
    33     Periode:
     31    <%= t('astat.period') %>
    3432    <% [7,14,30,90].forEach(function(n){ %>
    3533      <a href="/admin/stats?days=<%= n %>" class="<%= s.days === n ? 'is-on' : '' %>"><%= n %>d</a>
     
    3937  <section class="stats-chart-wrap">
    4038    <div class="stats-chart-head">
    41       <h2>Laatste <%= s.days %> dagen</h2>
     39      <h2><%= t('astat.last_n_days', { n: s.days }) %></h2>
    4240      <div class="stats-legend">
    43         <span class="lg lg-vis">Bezoeker-dagen</span>
    44         <span class="lg lg-pv">Weergaven</span>
     41        <span class="lg lg-vis"><%= t('astat.lg_visitor_days') %></span>
     42        <span class="lg lg-pv"><%= t('astat.lg_pageviews') %></span>
    4543      </div>
    4644    </div>
     
    5149           var visPct = Math.round(d.visitors / _max * 100);
    5250      %>
    53         <div class="sc-col" title="<%= d.day %> — <%= d.pageviews %> weergaven, <%= d.visitors %> bezoeker-dagen">
     51        <div class="sc-col" title="<%= t('astat.bar_title', { day: d.day, pv: d.pageviews, vd: d.visitors }) %>">
    5452          <div class="sc-bar">
    5553            <span class="sc-pv" style="height: <%= pvPct %>%"></span>
     
    6462  <div class="stats-tops">
    6563    <section class="stats-top">
    66       <h2>Populairste posts</h2>
     64      <h2><%= t('astat.top_posts') %></h2>
    6765      <% if (!s.topPosts.length || s.topPosts[0].views === 0) { %>
    68         <p class="stats-empty">Nog geen weergaven.</p>
     66        <p class="stats-empty"><%= t('astat.no_views') %></p>
    6967      <% } else { %>
    7068        <ol>
     
    7674    </section>
    7775    <section class="stats-top">
    78       <h2>Meest geluisterd</h2>
     76      <h2><%= t('astat.most_played') %></h2>
    7977      <% if (!s.topTracks.length || s.topTracks[0].plays === 0) { %>
    80         <p class="stats-empty">Nog geen plays.</p>
     78        <p class="stats-empty"><%= t('astat.no_plays') %></p>
    8179      <% } else { %>
    8280        <ol>
     
    9189  <% if (typeof s.referrers !== 'undefined' && s.referrers.length) { %>
    9290    <section class="stats-top" style="margin-top:1.2rem">
    93       <h2>🌐 Bronnen (waar bezoekers vandaan komen)</h2>
     91      <h2>🌐 <%= t('astat.sources') %></h2>
    9492      <ol>
    9593        <% s.referrers.forEach(function(r){ %>
     
    102100  <% if (typeof linkClicks !== 'undefined' && linkClicks.length) { %>
    103101    <section class="stats-top" style="margin-top:1.2rem">
    104       <h2>🔗 Link-in-bio klikken</h2>
     102      <h2>🔗 <%= t('astat.linkbio_clicks') %></h2>
    105103      <ol>
    106104        <% linkClicks.forEach(function(l){ %>
  • src/views/pages/admin-updates.ejs

    r929d98d rbbf9d1a  
    11<div class="container admin-page">
    2   <h1>Updates</h1>
    3   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
     2  <h1><%= t('aupd.title') %></h1>
     3  <p><a href="/admin" class="btn">&larr; <%= t('aupd.back_admin') %></a></p>
    44
    55  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
     
    77
    88  <section class="set-card">
    9     <h2>Versie van deze Klonkt</h2>
     9    <h2><%= t('aupd.version_heading') %></h2>
    1010
    1111    <div class="upd-row">
    12       <span class="upd-label">App-versie</span>
     12      <span class="upd-label"><%= t('aupd.app_version') %></span>
    1313      <span class="upd-val"><%= appVersion || '—' %></span>
    1414    </div>
    1515    <div class="upd-row">
    16       <span class="upd-label">Huidig</span>
     16      <span class="upd-label"><%= t('aupd.current') %></span>
    1717      <span class="upd-val">
    1818        <% if (currentSha) { %><code><%= currentSha %></code> <span class="upd-desc"><%= currentDesc %></span>
    19         <% } else { %><em>onbekend (nog niet via de update-knop bijgewerkt)</em><% } %>
     19        <% } else { %><em><%= t('aupd.current_unknown') %></em><% } %>
    2020      </span>
    2121    </div>
    2222    <div class="upd-row">
    23       <span class="upd-label">Nieuwste</span>
     23      <span class="upd-label"><%= t('aupd.latest') %></span>
    2424      <span class="upd-val">
    2525        <% if (latestSha) { %><code><%= latestSha %></code> <span class="upd-desc"><%= latestDesc %></span>
    26         <% } else { %><em>kon de nieuwste versie niet ophalen</em><% } %>
     26        <% } else { %><em><%= t('aupd.latest_failed') %></em><% } %>
    2727      </span>
    2828    </div>
     
    3030    <div class="upd-status">
    3131      <% if (!canCheck) { %>
    32         <span class="upd-badge warn">&#9888; Geen update-bron bereikbaar</span>
     32        <span class="upd-badge warn">&#9888; <%= t('aupd.no_source') %></span>
    3333      <% } else if (upToDate) { %>
    34         <span class="upd-badge ok">&#10003; Up-to-date</span>
     34        <span class="upd-badge ok">&#10003; <%= t('aupd.uptodate') %></span>
    3535      <% } else { %>
    36         <span class="upd-badge new">&#8635; Update beschikbaar<% if (behind) { %> (<%= behind %> commit<%= behind === '1' ? '' : 's' %> achter)<% } %></span>
     36        <span class="upd-badge new">&#8635; <%= t('aupd.update_available') %><% if (behind) { %> (<%= behind === '1' ? t('aupd.behind_one', { n: behind }) : t('aupd.behind_many', { n: behind }) %>)<% } %></span>
    3737      <% } %>
    3838    </div>
     
    4040    <% if (canCheck) { %>
    4141    <form method="post" action="/admin/updates/run" class="set-form"
    42           onsubmit="return confirm('De site wordt naar de nieuwste versie gebracht en herstart kort. Doorgaan?');">
     42          onsubmit="return confirm('<%= t('aupd.run_confirm') %>');">
    4343      <button type="submit" class="btn btn-primary">
    44         <%= upToDate ? '↻ Opnieuw uitrollen' : '⬆ Nu bijwerken' %>
     44        <%= upToDate ? '↻ ' + t('aupd.redeploy') : '⬆ ' + t('aupd.update_now') %>
    4545      </button>
    4646    </form>
     
    4848
    4949    <p class="set-help" style="margin-top:1rem">
    50       Bijwerken haalt de nieuwste code op en herstart deze site kort (~10s). Doe dit
    51       rustig — er gaat niets verloren (je posts, instellingen en cirkel blijven staan).
     50      <%= t('aupd.help') %>
    5251    </p>
    5352  </section>
  • src/views/pages/admin-users.ejs

    r929d98d rbbf9d1a  
    11<div class="container ax-page">
    22
    3   <p><a href="/admin" class="btn">&larr; Beheer</a></p>
     3  <p><a href="/admin" class="btn">&larr; <%= t('ausr.back') %></a></p>
    44  <header class="ax-header">
    55    <div>
    6       <h1>Users</h1>
    7       <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen. Rol <strong>kijker</strong> = alles bekijken (incl. Beheer), niets wijzigen — handig voor demo's.</p>
     6      <h1><%= t('ausr.title') %></h1>
     7      <p class="ax-tagline"><%= t('ausr.tagline_a') %> <strong><%= t('ausr.role_kijker') %></strong> <%= t('ausr.tagline_b') %></p>
    88    </div>
    99  </header>
     
    1616      <div class="ax-empty">
    1717        <div class="ax-empty-icon">👤</div>
    18         <p>Geen gebruikers.</p>
     18        <p><%= t('ausr.empty') %></p>
    1919      </div>
    2020    </div>
    2121  <% } else { %>
     22    <%
     23      var _u_self      = t('ausr.you');
     24      var _u_t_sites   = t('ausr.t_sites');
     25      var _u_t_posts   = t('ausr.t_posts');
     26      var _u_t_joined  = t('ausr.t_joined');
     27      var _u_l_sites   = t('ausr.l_sites');
     28      var _u_l_posts   = t('ausr.l_posts');
     29      var _u_l_joined  = t('ausr.l_joined');
     30      var _u_newklonkt = t('ausr.new_klonkt');
     31      var _u_rol       = t('ausr.role_label');
     32      var _u_r_kijker  = t('ausr.role_kijker');
     33      var _u_r_member  = t('ausr.role_member');
     34      var _u_r_admin   = t('ausr.role_admin');
     35      var _u_r_god     = t('ausr.role_god');
     36      var _u_del       = t('ausr.delete');
     37    %>
    2238    <ul class="ax-list">
    2339      <% users.forEach(function(u) { %>
     
    3349              <div class="ax-user-name">
    3450                <a href="/users/<%= encodeURIComponent(u.username) %>"><%= u.username %></a>
    35                 <% if (u.id === user.id) { %><span class="ax-pill">jij</span><% } %>
     51                <% if (u.id === user.id) { %><span class="ax-pill"><%= _u_self %></span><% } %>
    3652              </div>
    3753              <div class="ax-user-email"><%= u.email %></div>
     
    4056
    4157          <div class="ax-user-stats">
    42             <span class="ax-stat" title="Sites">
     58            <span class="ax-stat" title="<%= _u_t_sites %>">
    4359              <span class="ax-stat-num"><%= u.site_count %></span>
    44               <span class="ax-stat-label">sites</span>
     60              <span class="ax-stat-label"><%= _u_l_sites %></span>
    4561            </span>
    46             <span class="ax-stat" title="Posts">
     62            <span class="ax-stat" title="<%= _u_t_posts %>">
    4763              <span class="ax-stat-num"><%= u.post_count %></span>
    48               <span class="ax-stat-label">posts</span>
     64              <span class="ax-stat-label"><%= _u_l_posts %></span>
    4965            </span>
    50             <span class="ax-stat" title="Geregistreerd op">
     66            <span class="ax-stat" title="<%= _u_t_joined %>">
    5167              <span class="ax-stat-num ax-stat-date"><%= formatDate(u.created_at) %></span>
    52               <span class="ax-stat-label">joined</span>
     68              <span class="ax-stat-label"><%= _u_l_joined %></span>
    5369            </span>
    5470          </div>
     
    5975            <% if (typeof tenancy !== 'undefined' && tenancy === 'hub' && canMutate) { %>
    6076              <a href="/admin/sites/new?owner=<%= u.id %>" class="ax-icon-btn"
    61                  aria-label="Nieuwe Klonkt voor <%= u.username %>" title="Nieuwe Klonkt voor deze gebruiker">+🌐</a>
     77                 aria-label="<%= t('ausr.new_klonkt_for', { name: u.username }) %>" title="<%= _u_newklonkt %>">+🌐</a>
    6278            <% } %>
    6379            <form method="post" action="/admin/users/<%= u.id %>/role" class="ax-role-form">
    6480              <label class="ax-role">
    65                 <span>Rol</span>
     81                <span><%= _u_rol %></span>
    6682                <select name="role" onchange="this.form.submit()" <% if (u.id === user.id || !canMutate) { %>disabled<% } %>>
    67                   <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>>kijker</option>
    68                   <option value="member" <%= u.role === 'member' ? 'selected' : '' %>>member</option>
    69                   <option value="admin"  <%= u.role === 'admin'  ? 'selected' : '' %>>admin</option>
    70                   <option value="god"    <%= u.role === 'god'    ? 'selected' : '' %>>god</option>
     83                  <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>><%= _u_r_kijker %></option>
     84                  <option value="member" <%= u.role === 'member' ? 'selected' : '' %>><%= _u_r_member %></option>
     85                  <option value="admin"  <%= u.role === 'admin'  ? 'selected' : '' %>><%= _u_r_admin %></option>
     86                  <option value="god"    <%= u.role === 'god'    ? 'selected' : '' %>><%= _u_r_god %></option>
    7187                </select>
    7288              </label>
     
    7591            <% if (u.id !== user.id && canMutate) { %>
    7692              <% var _warn = (u.post_count + u.site_count > 0)
    77                    ? ('Dit verwijdert ook hun site + ' + u.post_count + ' post(s). ')
     93                   ? (t('ausr.del_warn', { n: u.post_count }) + ' ')
    7894                   : ''; %>
    7995              <form method="post" action="/admin/users/<%= u.id %>/delete" class="ax-delete-form"
    80                     onsubmit="return confirm('Gebruiker <%= u.username %> verwijderen? <%= _warn %>Dit kan niet ongedaan worden.')">
    81                 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
     96                    onsubmit="return confirm('<%= t('ausr.del_confirm', { name: u.username }) %> <%= _warn %><%= t('ausr.del_undo') %>')">
     97                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _u_del %>" title="<%= _u_del %>">🗑</button>
    8298              </form>
    8399            <% } else { %>
Note: See TracChangeset for help on using the changeset viewer.