Changeset 03fa548 in Klonkt


Ignore:
Timestamp:
06/20/2026 01:39:41 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
91c0a39
Parents:
d0f4c10
Message:

feat(i18n): interface language NL/EN/DE with visitor switcher (phase 1: chrome)

  • i18n.js (t helper + nl/en/de dictionary + resolveLang: session → browser → nl).
  • render.js passes t/lang/langs to all views; /lang/:code sets the language in the session.
  • Language switcher (select) in the footer; chrome translated: topnav (Archive/Login/Account/Admin/Logout/…), pill (Agenda/Solo/Circle/Timeline/Grid), post nav (Newer/Older), footer.
  • Content (posts/site name/bio) stays in the author's language. Admin + deeper page strings follow in a next phase.

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

Location:
src
Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • src/middleware/render.js

    rd0f4c10 r03fa548  
    2020import { isPremium as isPremiumInstance, premiumEnabled, premiumUnlocked } from '../services/PatreonService.js';
    2121import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js';
     22import { t as i18nT, resolveLang, SUPPORTED as LANGS, LANG_NAMES } from '../services/i18n.js';
    2223
    2324const __dirname = path.dirname(fileURLToPath(import.meta.url));
     
    7778  const canSeeBeheer = !!(_u && (_role === 'god' || _role === 'admin' || _role === 'kijker' || userOwnsSite));
    7879
     80  // Interface-taal (bezoeker-instelbaar): sessie-keuze → browser-taal → nl.
     81  const _lang = resolveLang(req);
     82
    7983  // Common locals
    8084  const locals = {
    8185    user: _u,
     86    lang: _lang,
     87    t: (key, vars) => i18nT(_lang, key, vars),
     88    langs: LANGS.map((c) => ({ code: c, name: LANG_NAMES[c], active: c === _lang })),
    8289    userOwnsSite,
    8390    canSeeBeheer,
  • src/server.js

    rd0f4c10 r03fa548  
    4646import artistsRoutes from './routes/artists.js';
    4747import postsRoutes from './routes/posts.js';
     48import langRoutes from './routes/lang.js';
    4849import federationRoutes from './routes/federation.js';
    4950import { startCircleSyncLoop } from './services/CircleService.js';
     
    288289app.use('/', showsRoutes); // /shows agenda + notify-me (premium)
    289290app.use('/', changelogRoutes); // /changelog publieke release-/wijzigingen-pagina
     291app.use('/', langRoutes); // /lang/:code — interface-taal kiezen (vóór de catch-all)
    290292app.use('/', postsRoutes);
    291293
  • src/views/partials/footer.ejs

    rd0f4c10 r03fa548  
    22  <% if (typeof footerNewsletter !== 'undefined' && footerNewsletter && (typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof site !== 'undefined' && site) { %>
    33    <div class="container footer-nl">
    4       <span class="footer-nl-label">📬 Blijf op de hoogte</span>
     4      <span class="footer-nl-label">📬 <%= t('footer.subscribe_cta') %></span>
    55      <form method="post" action="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief" class="footer-nl-form">
    6         <input type="email" name="email" required placeholder="jouw@email.nl" autocomplete="email" aria-label="E-mailadres">
    7         <button type="submit">Inschrijven</button>
     6        <input type="email" name="email" required placeholder="<%= t('common.email_placeholder') %>" autocomplete="email" aria-label="E-mailadres">
     7        <button type="submit"><%= t('footer.subscribe') %></button>
    88      </form>
    99    </div>
     
    1717    <button type="button" class="install-app-btn install-app-btn--footer" data-pcms-install-app>
    1818      <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 20h14v-2H5v2zm7-18L5.33 9h3.84v6h5.66V9h3.84L12 2z" fill="currentColor"/></svg>
    19       <span>Installeer app</span>
     19      <span><%= t('footer.install') %></span>
    2020    </button>
    2121    <div class="footer-meta">
     22      <% if (typeof langs !== 'undefined' && langs && langs.length > 1) { %>
     23        <select class="footer-lang" aria-label="<%= t('nav.language') %>"
     24                onchange="location.href='/lang/'+this.value+'?r='+encodeURIComponent(location.pathname+location.search)">
     25          <% langs.forEach(function(l){ %><option value="<%= l.code %>"<%= l.active ? ' selected' : '' %>><%= l.name %></option><% }); %>
     26        </select>
     27      <% } %>
    2228      <a class="footer-version" href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/changelog" title="Bekijk wat er per release veranderd is">Klonkt Beta<%= (typeof appVersion !== 'undefined' && appVersion) ? ' v' + appVersion : '' %></a>
    2329      <%# Mobile-only theme toggle. On desktop the topnav has its own toggle
     
    101107  display: inline-flex; align-items: center; gap: 0.75rem;
    102108}
     109.footer-lang {
     110  background: transparent; color: var(--ink); opacity: 1;
     111  border: 1px solid var(--rule); border-radius: 999px;
     112  padding: 0.3rem 0.5rem; font-size: 0.78rem; cursor: pointer;
     113  font-family: inherit;
     114}
     115.footer-lang:hover { border-color: var(--accent); }
    103116.footer-version { color: inherit; text-decoration: none; }
    104117.footer-version:hover { color: var(--accent); text-decoration: underline; }
  • src/views/partials/post-nav.ejs

    rd0f4c10 r03fa548  
    88         hx-get="/<%= newerPost.slug %>?partial=1" hx-target="#pcms-main"
    99         hx-push-url="/<%= newerPost.slug %>" hx-indicator="#pcms-loading"<% } %>>
    10         <span class="post-nav-label"><% if (newerPost.pinned) { %><span class="post-nav-pin" title="Vastgepind">📌</span> <% } %>← Newer</span>
     10        <span class="post-nav-label"><% if (newerPost.pinned) { %><span class="post-nav-pin" title="Vastgepind">📌</span> <% } %>← <%= t('postnav.newer') %></span>
    1111        <span class="post-nav-title"><%= newerPost.title %></span>
    1212      </a>
     
    1414      <div class="post-nav-prev is-empty" aria-hidden="true">
    1515        <span class="post-nav-label">← Newer</span>
    16         <span class="post-nav-title">Nieuwste post</span>
     16        <span class="post-nav-title"><%= t('postnav.newest') %></span>
    1717      </div>
    1818    <% } %>
     
    2121         hx-get="/<%= olderPost.slug %>?partial=1" hx-target="#pcms-main"
    2222         hx-push-url="/<%= olderPost.slug %>" hx-indicator="#pcms-loading"<% } %>>
    23         <span class="post-nav-label">Older →<% if (olderPost.pinned) { %> <span class="post-nav-pin" title="Vastgepind">📌</span><% } %></span>
     23        <span class="post-nav-label"><%= t('postnav.older') %> →<% if (olderPost.pinned) { %> <span class="post-nav-pin" title="Vastgepind">📌</span><% } %></span>
    2424        <span class="post-nav-title"><%= olderPost.title %></span>
    2525      </a>
     
    2727      <div class="post-nav-next is-empty" aria-hidden="true">
    2828        <span class="post-nav-label">Older →</span>
    29         <span class="post-nav-title">Oudste post</span>
     29        <span class="post-nav-title"><%= t('postnav.oldest') %></span>
    3030      </div>
    3131    <% } %>
  • src/views/partials/topnav.ejs

    rd0f4c10 r03fa548  
    2626    <div class="masthead-brand">
    2727      <% if (_isAdmin) { %>
    28         <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin">← Terug naar site</a>
     28        <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
    2929      <% } else if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
    3030        <%# Hub: de brand gaat naar de hub-home via HTMX. De chrome wordt in hub-modus
     
    6464           hx-indicator="#pcms-loading">
    6565          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8v13H3V8"/><rect x="1" y="3" width="22" height="5" rx="1"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
    66           <span class="nav-label">Archief</span>
     66          <span class="nav-label"><%= t('nav.archive') %></span>
    6767        </a>
    6868      <% } %>
     
    118118                 hx-push-url="/account" hx-indicator="#pcms-loading">
    119119                <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
    120                 <span>Account</span>
     120                <span><%= t('nav.account') %></span>
    121121              </a>
    122122              <a href="/favorieten" role="menuitem" class="udi"
     
    124124                 hx-push-url="/favorieten" hx-indicator="#pcms-loading">
    125125                <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
    126                 <span>Favorieten</span>
     126                <span><%= t('nav.favorites') %></span>
    127127              </a>
    128128              <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
     
    131131                   hx-push-url="/admin" hx-indicator="#pcms-loading">
    132132                  <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3z"/><path d="M14 3h7v7h-7z"/><path d="M14 14h7v7h-7z"/><path d="M3 14h7v7H3z"/></svg>
    133                   <span>Beheer</span>
     133                  <span><%= t('nav.admin') %></span>
    134134                </a>
    135135              <% } %>
     
    139139                   hx-push-url="<%= _siteUrlBase %>/posts/new" hx-indicator="#pcms-loading">
    140140                  <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
    141                   <span>Nieuwe post</span>
     141                  <span><%= t('nav.new_post') %></span>
    142142                </a>
    143143              <% } %>
     
    147147              <a href="/auth/logout" role="menuitem" class="udi udi-danger">
    148148                <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
    149                 <span>Uitloggen</span>
     149                <span><%= t('nav.logout') %></span>
    150150              </a>
    151151            </div>
     
    156156           hx-get="/auth/login?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
    157157           hx-push-url="/auth/login" hx-indicator="#pcms-loading">
    158           <span class="nav-label">Inloggen</span>
     158          <span class="nav-label"><%= t('nav.login') %></span>
    159159        </a>
    160160      <% } %>
  • src/views/partials/view-switcher.ejs

    rd0f4c10 r03fa548  
    1919      <a class="vs-agenda<%= _onShows ? ' is-active' : '' %>" href="<%= _sbA %>/shows" aria-label="Agenda — evenementen">
    2020        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
    21         <span>Agenda</span>
     21        <span><%= t('switch.agenda') %></span>
    2222      </a>
    2323    </div>
     
    2929    <div class="feed-scope" role="tablist" aria-label="Solo of cirkel">
    3030      <a class="feed-scope-btn<%= (!_neutral && !_onCirkel) ? ' is-active' : '' %>" href="<%= _sb %>/"
    31          role="tab" aria-selected="<%= (!_neutral && !_onCirkel) ? 'true' : 'false' %>">Solo</a>
     31         role="tab" aria-selected="<%= (!_neutral && !_onCirkel) ? 'true' : 'false' %>"><%= t('switch.solo') %></a>
    3232      <a class="feed-scope-btn<%= (!_neutral && _onCirkel) ? ' is-active' : '' %>" href="<%= _sb %>/cirkel"
    33          role="tab" aria-selected="<%= (!_neutral && _onCirkel) ? 'true' : 'false' %>">Cirkel</a>
     33         role="tab" aria-selected="<%= (!_neutral && _onCirkel) ? 'true' : 'false' %>"><%= t('switch.circle') %></a>
    3434    </div>
    3535    <% } %>
     
    3838              aria-selected="<%= (!_neutral && !(typeof site !== 'undefined' && site && site.feed_view_default === 'grid')) ? 'true' : 'false' %>">
    3939        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
    40         <span>Tijdlijn</span>
     40        <span><%= t('switch.timeline') %></span>
    4141      </button>
    4242      <button type="button" class="view-switch-btn" data-view="grid" role="tab"
    4343              aria-selected="<%= (!_neutral && (typeof site !== 'undefined' && site && site.feed_view_default === 'grid')) ? 'true' : 'false' %>">
    4444        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
    45         <span>Grid</span>
     45        <span><%= t('switch.grid') %></span>
    4646      </button>
    4747    </div>
Note: See TracChangeset for help on using the changeset viewer.