Ignore:
Timestamp:
07/22/2026 08:18:39 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
6b5d7da
Parents:
96c714f
git-author:
Robin <roboburr@…> (07/22/2026 08:18:19 AM)
git-committer:
Robin <roboburr@…> (07/22/2026 08:18:39 AM)
Message:

Fix: test feedback round, cirkels tagline + report details + full i18n

Three findings from Bart's test pass, tied together by the new i18n keys:

  1. The admin tagline said "Solo mode" even with Cirkels (federation) on. Solo-tenancy now distinguishes: apEnabled -> "Cirkels-modus: jouw site, verbonden met de fediverse", else the solo line. New key in nl/en/de.
  1. Reports only showed who reported; now they show the reason and WHICH post. getNotifications resolves the stored objects URIs of a report: our own /ap/notes/<id> become "Over de post: <title>" links under the description; an empty reason renders "Geen reden opgegeven". Other URIs (the actor itself) are skipped, the row already names the account.
  1. Notificaties and Betaalde posts were hardcoded Dutch on an English-configured Klonkt. Everything now goes through t(): the two admin pages (including their client-side status strings, passed via the JSON data block), the admin menu buttons, and the visitor-facing paid pages (gate, passkey, result). Server-side push payloads translate too, using the SITE's content language (fallback KLONKT_DEFAULT_LANG), and the test ping uses the request language. Full nl/en/de dictionaries.

Changed files:
src/services/i18n.js

  • admin.tagline_cirkels, admin.b_paid/b_push/back, notif.report_about/ report_noreason, and the push.*, apaid.*, pgate.*, ppk.*, pres.* blocks in nl/en/de

src/routes/admin.js, src/views/pages/admin.ejs

  • circlesOn (apEnabled) -> cirkels tagline; menu buttons via t()

src/services/ActivityPubService.js

  • report objects resolved to post links; push payloads via i18nT with pushLang(slug) (site language)

src/views/pages/fedi-notifications.ejs

  • report reason fallback + "Over de post" links (+ styles)

src/routes/admin-push.js, src/views/pages/admin-push.ejs

  • pageTitleKey push.t; all copy + JS status strings via t()

src/routes/admin-paid.js, src/views/pages/admin-paid.ejs

  • pageTitleKey apaid.t; all copy via t(), copy-button label via data-attr

src/routes/paid.js

  • pageTitleKey pres.t / ppk.t

src/views/pages/paid-gate.ejs, paid-passkey.ejs, paid-result.ejs

  • visitor copy + JS strings via t()

src/routes/push.js

  • test notification via resolveLang(req)

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r96c714f r9a00f28  
    2424import Push from './PushService.js';
    2525import { getTenancy } from './SettingsService.js';
     26import { t as i18nT } from './i18n.js';
    2627
    2728const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
     
    12731274  try { return getTenancy() === 'hub' ? `/user/${slug}` : ''; } catch { return ''; }
    12741275}
     1276// Notification language: the site's content language (fallback: instance default).
     1277function pushLang(slug) {
     1278  try { const r = db.prepare('SELECT language FROM sites WHERE slug = ?').get(slug); return (r && r.language) || process.env.KLONKT_DEFAULT_LANG || 'nl'; } catch { return 'nl'; }
     1279}
    12751280// Site slug, target URL and title for a post-scoped notification.
    12761281function pushPostCtx(postId) {
     
    13431348    fStmts().ins.run(slug, who, remote.inbox, sharedInbox, fi.name, fi.handle, fi.icon);
    13441349    try { _updFDisp.run(fi.name, fi.handle, fi.icon, slug, who); } catch { /* best effort */ }
    1345     pushEvent(slug, { type: 'follow', title: 'Nieuwe volger', body: `${fi.name || fi.handle || 'Iemand'} volgt je nu`, url: `${pushPrefix(slug)}/connect` });
     1350    { const L = pushLang(slug); pushEvent(slug, { type: 'follow', title: i18nT(L, 'push.n_follow_t'), body: i18nT(L, 'push.n_follow_b', { who: fi.name || fi.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(slug)}/connect` }); }
    13461351    const me = actorId(base, slug);
    13471352    const keys = getOrCreateKeys(slug);
     
    14131418        const vis = noteVisibility(o);
    14141419        const priv = vis === 'direct' || vis === 'followers';
    1415         const who = ai.name || ai.handle || 'Iemand';
    14161420        if (ctx) {
    1417           if (priv) pushEvent(ctx.site, { type: 'dm', title: 'Privébericht', body: `Nieuw bericht van ${who}`, url: `${pushPrefix(ctx.site)}/messages` });
    1418           else pushEvent(ctx.site, { type: 'reply', title: `Reactie op "${ctx.title}"`, body: `${who}: ${HtmlSanitizerService.toPlainText(html).slice(0, 90)}`, url: ctx.url });
     1421          const L = pushLang(ctx.site);
     1422          const who = ai.name || ai.handle || i18nT(L, 'notif.someone');
     1423          if (priv) pushEvent(ctx.site, { type: 'dm', title: i18nT(L, 'push.n_dm_t'), body: i18nT(L, 'push.n_dm_b', { who }), url: `${pushPrefix(ctx.site)}/messages` });
     1424          else pushEvent(ctx.site, { type: 'reply', title: i18nT(L, 'push.n_reply_t', { title: ctx.title }), body: `${who}: ${HtmlSanitizerService.toPlainText(html).slice(0, 90)}`, url: ctx.url });
    14191425        }
    14201426      }
     
    14651471              const vis = noteVisibility(o);
    14661472              const priv = vis === 'direct' || vis === 'followers';
    1467               const who = ai.name || ai.handle || 'Iemand';
     1473              const L = pushLang(slug);
     1474              const who = ai.name || ai.handle || i18nT(L, 'notif.someone');
    14681475              // Same privacy rule as replies: private mentions push without content.
    1469               if (priv) pushEvent(slug, { type: 'dm', title: 'Privébericht', body: `Nieuw bericht van ${who}`, url: `${pushPrefix(slug)}/messages` });
    1470               else pushEvent(slug, { type: 'reply', title: 'Vermelding', body: `${who}: ${HtmlSanitizerService.toPlainText(html).slice(0, 90)}`, url: `${pushPrefix(slug)}/messages` });
     1476              if (priv) pushEvent(slug, { type: 'dm', title: i18nT(L, 'push.n_dm_t'), body: i18nT(L, 'push.n_dm_b', { who }), url: `${pushPrefix(slug)}/messages` });
     1477              else pushEvent(slug, { type: 'reply', title: i18nT(L, 'push.n_mention_t'), body: `${who}: ${HtmlSanitizerService.toPlainText(html).slice(0, 90)}`, url: `${pushPrefix(slug)}/messages` });
    14711478            }
    14721479          } catch { /* ignore */ }
     
    15261533      {
    15271534        const ctx = pushPostCtx(pid);
    1528         const who = ai.name || ai.handle || 'Iemand';
    15291535        if (ctx) {
    1530           if (type === 'Like') pushEvent(ctx.site, { type: 'like', title: 'Nieuwe waardering', body: `${who} waardeerde "${ctx.title}"`, url: ctx.url });
    1531           else pushEvent(ctx.site, { type: 'boost', title: 'Geboost', body: `${who} boostte "${ctx.title}"`, url: ctx.url });
     1536          const L = pushLang(ctx.site);
     1537          const who = ai.name || ai.handle || i18nT(L, 'notif.someone');
     1538          if (type === 'Like') pushEvent(ctx.site, { type: 'like', title: i18nT(L, 'push.n_like_t'), body: i18nT(L, 'push.n_like_b', { who, title: ctx.title }), url: ctx.url });
     1539          else pushEvent(ctx.site, { type: 'boost', title: i18nT(L, 'push.n_boost_t'), body: i18nT(L, 'push.n_boost_b', { who, title: ctx.title }), url: ctx.url });
    15321540        }
    15331541      }
     
    29692977  } catch { /* ignore */ }
    29702978  try {
    2971     for (const r of db.prepare('SELECT actor_uri, actor_name, actor_handle, actor_icon, content, created_at FROM ap_reports WHERE slug = ? ORDER BY created_at DESC LIMIT ?').all(slug, L)) {
    2972       out.push({ type: 'report', name: r.actor_name, handle: r.actor_handle, url: r.actor_uri, icon: r.actor_icon, content: r.content, created_at: r.created_at });
     2979    for (const r of db.prepare('SELECT actor_uri, actor_name, actor_handle, actor_icon, content, objects, created_at FROM ap_reports WHERE slug = ? ORDER BY created_at DESC LIMIT ?').all(slug, L)) {
     2980      // The reported objects: our own notes resolve to post links so the owner
     2981      // sees WHICH post the report is about; other URIs (e.g. the actor itself)
     2982      // are skipped — the report row already names the account.
     2983      const about = [];
     2984      try {
     2985        for (const u of JSON.parse(r.objects || '[]')) {
     2986          const m = String(u).match(/\/ap\/notes\/([^/?#]+)/);
     2987          if (!m) continue;
     2988          const p = db.prepare('SELECT slug, title FROM posts WHERE id = ?').get(decodeURIComponent(m[1]));
     2989          if (p) about.push({ slug: p.slug, title: p.title || p.slug });
     2990        }
     2991      } catch { /* malformed objects json → no links */ }
     2992      out.push({ type: 'report', name: r.actor_name, handle: r.actor_handle, url: r.actor_uri, icon: r.actor_icon, content: r.content, objects: about, created_at: r.created_at });
    29732993    }
    29742994  } catch { /* ignore */ }
Note: See TracChangeset for help on using the changeset viewer.