Changeset 0688b5f in Klonkt
- Timestamp:
- 07/01/2026 07:06:45 PM (2 months ago)
- Branches:
- main
- Children:
- 1c2dcba
- Parents:
- d18c60e
- Files:
-
- 1 added
- 10 edited
-
CHANGELOG.de.md (modified) (1 diff)
-
CHANGELOG.md (modified) (1 diff)
-
CHANGELOG.nl.md (modified) (1 diff)
-
src/config/database.js (modified) (1 diff)
-
src/routes/posts.js (modified) (7 diffs)
-
src/services/ActivityPubService.js (modified) (1 diff)
-
src/services/Scheduler.js (modified) (2 diffs)
-
src/services/i18n.js (modified) (3 diffs)
-
src/views/pages/post-edit.ejs (modified) (1 diff)
-
test/activitypub-as2.test.js (modified) (3 diffs)
-
test/post-language.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.de.md
rd18c60e r0688b5f 7 7 8 8 ### Hinzugefügt 9 - **Sprache eines Beitrags festlegen.** Wähle, in welcher Sprache du einen Beitrag geschrieben hast — 10 im Fediverse funktionieren damit der Sprachfilter der Timeline und die Übersetzen-Schaltfläche. 9 11 - **Alt-Text für Bilder.** Gib deinem Titelbild eine Beschreibung (Inline-Bilder behalten ihren 10 12 eigenen Alt-Text) — sie föderiert ins Fediverse und lässt Screenreader das Bild beschreiben. -
CHANGELOG.md
rd18c60e r0688b5f 7 7 8 8 ### Added 9 - **Set a post's language.** Choose the language you wrote a post in — on the fediverse it enables 10 timeline language filtering and the translate button. 9 11 - **Alt text for images.** Give your cover image a description (and inline images keep their own 10 12 alt text) — it federates to the fediverse and lets screen readers describe the picture. -
CHANGELOG.nl.md
rd18c60e r0688b5f 7 7 8 8 ### Toegevoegd 9 - **Stel de taal van een post in.** Kies in welke taal je een post schreef — op de fediverse 10 werkt daarmee het taalfilter van de tijdlijn en de vertaal-knop. 9 11 - **Alt-tekst voor afbeeldingen.** Geef je cover een beschrijving (en inline-afbeeldingen behouden 10 12 hun eigen alt-tekst) — die federeert mee naar de fediverse en laat schermlezers de afbeelding beschrijven. -
src/config/database.js
rd18c60e r0688b5f 90 90 ensureColumn('posts', 'cover_video_url', 'TEXT'); // muted loop MP4 for an animated cover (Safari-smooth) 91 91 ensureColumn('posts', 'cover_alt', 'TEXT'); // alt text / description for the cover (a11y → AS2 attachment `name`) 92 ensureColumn('posts', 'language', 'TEXT'); // BCP-47 content language → federates as AS2 contentMap (Mastodon language filter/translate) 92 93 ensureColumn('posts', 'content_warning', 'TEXT'); // custom CW label (empty = default "Gevoelige inhoud") 93 94 ensureColumn('posts', 'type', "TEXT DEFAULT 'post'"); // post | foto | video | audio -
src/routes/posts.js
rd18c60e r0688b5f 247 247 const cw = (req.body.content_warning || '').trim().slice(0, 200); 248 248 const coverAlt = (req.body.cover_alt || '').trim().slice(0, 1500) || null; // cover alt text (a11y) 249 const language = /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/.test(req.body.language || '') ? req.body.language : (res.locals.lang || null); // BCP-47 content language 249 250 250 251 // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize … … 285 286 INSERT INTO posts ( 286 287 id, site_id, slug, author_id, title, content, excerpt, 287 status, cover_image_url, cover_video_url, cover_alt, pinned, tags, type, noindex, fan_only, nsfw, content_warning, poll_json, publish_at,288 status, cover_image_url, cover_video_url, cover_alt, language, pinned, tags, type, noindex, fan_only, nsfw, content_warning, poll_json, publish_at, 288 289 created_at, updated_at, published_at 289 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )290 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 290 291 `).run( 291 292 postId, site.id, finalSlug, req.session.user.id, 292 293 title || finalSlug, cleanContent, excerpt || '', 293 finalStatus, cover_image_url || null, (req.body.cover_video_url || null), coverAlt, parsePinnedRank(pinned),294 finalStatus, cover_image_url || null, (req.body.cover_video_url || null), coverAlt, language, parsePinnedRank(pinned), 294 295 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 295 296 finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt, … … 313 314 ActivityPubService.deliverCreate(site, { 314 315 id: postId, slug: finalSlug, title: title || finalSlug, 315 content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt, 316 content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt, language, 316 317 published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson, 317 318 }).catch(() => { /* best-effort */ }); … … 381 382 const cw = (req.body.content_warning || '').trim().slice(0, 200); 382 383 const coverAlt = (req.body.cover_alt || '').trim().slice(0, 1500) || null; // cover alt text (a11y) 384 const language = /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/.test(req.body.language || '') ? req.body.language : (res.locals.lang || null); // BCP-47 content language 383 385 const newSlug = req.body.slug; 384 386 const action = req.body.action || 'save'; … … 424 426 UPDATE posts SET 425 427 title = ?, content = ?, excerpt = ?, status = ?, 426 cover_image_url = ?, cover_video_url = ?, cover_alt = ?, pinned = ?, tags = ?,428 cover_image_url = ?, cover_video_url = ?, cover_alt = ?, language = ?, pinned = ?, tags = ?, 427 429 type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, poll_json = ?, publish_at = ?, 428 430 slug = ?, published_at = ?, updated_at = ? … … 430 432 `).run( 431 433 title, cleanContent, excerpt, finalStatus, 432 cover_image_url || null, (req.body.cover_video_url || null), coverAlt, parsePinnedRank(pinned),434 cover_image_url || null, (req.body.cover_video_url || null), coverAlt, language, parsePinnedRank(pinned), 433 435 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 434 436 finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt, … … 456 458 const apPost = { 457 459 id: post.id, slug: finalSlug, title: title || finalSlug, 458 content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt, 460 content: cleanContent, cover_image_url: cover_image_url || null, cover_video_url: req.body.cover_video_url || null, cover_alt: coverAlt, language, 459 461 published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson, 460 462 }; -
src/services/ActivityPubService.js
rd18c60e r0688b5f 400 400 // make it JSON-LD-clean with a context term, otherwise it degrades to the player card. 401 401 if (playable) note.embedUrl = `${base}/embed?post=${encodeURIComponent(post.slug)}`; 402 // Content language → AS2 contentMap (a BCP-47-keyed copy of the content). Mastodon reads the 403 // language from its key for the timeline language filter + the translate button. Emitted 404 // alongside `content` (Mastodon sends both); a plain receiver just uses `content`. 405 if (post.language && /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/.test(post.language)) note.contentMap = { [post.language]: note.content }; 402 406 // A hosted poll → federate as an AS2 Question (options + live tally). Do this last so it 403 407 // reuses the note's content/addressing/tags, then swaps the type and strips media. -
src/services/Scheduler.js
rd18c60e r0688b5f 15 15 try { 16 16 const due = db.prepare(` 17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.cover_video_url, p.cover_alt, p. fan_only, p.nsfw, p.content_warning, p.poll_json,17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.cover_video_url, p.cover_alt, p.language, p.fan_only, p.nsfw, p.content_warning, p.poll_json, 18 18 p.published_at, p.publish_at, p.created_at, u.username 19 19 FROM posts p JOIN users u ON u.id = p.author_id … … 38 38 ActivityPubService.deliverCreate(site, { 39 39 id: p.id, slug: p.slug, title: p.title || p.slug, 40 content: p.content, cover_image_url: p.cover_image_url || null, cover_video_url: p.cover_video_url || null, cover_alt: p.cover_alt, 40 content: p.content, cover_image_url: p.cover_image_url || null, cover_video_url: p.cover_video_url || null, cover_alt: p.cover_alt, language: p.language, 41 41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, content_warning: p.content_warning, poll_json: p.poll_json, 42 42 }).catch(() => { /* best-effort */ }); -
src/services/i18n.js
rd18c60e r0688b5f 639 639 'pedit.f_cover_alt': 'Alt-tekst (beschrijving)', 640 640 'pedit.cover_alt_placeholder': 'Beschrijf de afbeelding voor schermlezers', 641 'pedit.f_language': 'Taal', 642 'pedit.language_hint': 'voor het fediverse-taalfilter', 641 643 'pedit.cover_upload_btn': 'Upload nieuwe cover', 642 644 'pedit.s_content': 'Content', … … 1559 1561 'pedit.f_cover_alt': 'Alt text (description)', 1560 1562 'pedit.cover_alt_placeholder': 'Describe the image for screen readers', 1563 'pedit.f_language': 'Language', 1564 'pedit.language_hint': 'for the fediverse language filter', 1561 1565 'pedit.cover_upload_btn': 'Upload new cover', 1562 1566 'pedit.s_content': 'Content', … … 2479 2483 'pedit.f_cover_alt': 'Alt-Text (Beschreibung)', 2480 2484 'pedit.cover_alt_placeholder': 'Beschreibe das Bild für Screenreader', 2485 'pedit.f_language': 'Sprache', 2486 'pedit.language_hint': 'für den Fediverse-Sprachfilter', 2481 2487 'pedit.cover_upload_btn': 'Neues Titelbild hochladen', 2482 2488 'pedit.s_content': 'Inhalt', -
src/views/pages/post-edit.ejs
rd18c60e r0688b5f 87 87 <textarea name="excerpt" rows="2"><%= post.excerpt %></textarea> 88 88 <small class="pe-hint" style="opacity:.7"><%- t('pedit.excerpt_hint') %></small> 89 </label> 90 <% var _postLang = (typeof post.language !== 'undefined' && post.language) ? post.language : (typeof lang !== 'undefined' ? lang : 'en'); %> 91 <label class="pe-field"> 92 <span><%= t('pedit.f_language') %> <small><%= t('pedit.language_hint') %></small></span> 93 <select name="language" id="pe-language" style="max-width:220px"> 94 <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %> 95 <option value="<%= l[0] %>" <%= l[0] === _postLang ? 'selected' : '' %>><%= l[1] %></option> 96 <% }); %> 97 </select> 89 98 </label> 90 99 </section> -
test/activitypub-as2.test.js
rd18c60e r0688b5f 27 27 'published', 'updated', 'attributedTo', 'inReplyTo', 'replies', 28 28 'attachment', 'tag', 'icon', 'image', 'duration', 29 'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary 29 30 'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev', 30 31 'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox', … … 47 48 keys.add(k); 48 49 if (k === 'type' && typeof v === 'string') keys.add(v); 50 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms 49 51 collect(v, keys); 50 52 } … … 70 72 id: 'p1', slug: 'hello', title: 'Hi', content: '<p>hello #music</p>', 71 73 nsfw: 1, content_warning: 'cw', tags: JSON.stringify(['mood']), 72 cover_image_url: '/media/c.webp', 74 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en', 73 75 published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z', 74 76 };
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)