Changeset 0688b5f in Klonkt for src/routes/posts.js


Ignore:
Timestamp:
07/01/2026 07:06:45 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
1c2dcba
Parents:
d18c60e
Message:

feat(fediverse): post language → AS2 contentMap

A post can now carry a language; it federates as an AS2 contentMap (a BCP-47-keyed
copy of the content, alongside plain content) so Mastodon's timeline language filter
and translate button work. Editor gets a language picker (defaults to the author's
UI language).

  • src/config/database.js — posts.language column.
  • src/services/ActivityPubService.js — buildNote emits contentMap { <lang>: content } when the post has a valid BCP-47 language.
  • src/routes/posts.js — capture/validate/store language on create/save (default = the author's current language) and pass it to the federation hooks.
  • src/services/Scheduler.js — carry language when a scheduled post goes live.
  • src/views/pages/post-edit.ejs — language <select> (18 common languages).
  • src/services/i18n.js — pedit.f_language + pedit.language_hint (nl/en/de).
  • test/activitypub-as2.test.js — allow contentMap/nameMap/summaryMap; don't treat a language-map's keys as vocab terms; kitchen-sink post now sets a language.
  • test/post-language.test.js — contentMap shape, no-language, invalid-code = ignored.
  • CHANGELOG(.nl/.de).md — "Set a post's language" under Unreleased.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    rd18c60e r0688b5f  
    247247  const cw = (req.body.content_warning || '').trim().slice(0, 200);
    248248  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
    249250
    250251  // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize
     
    285286    INSERT INTO posts (
    286287      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,
    288289      created_at, updated_at, published_at
    289     ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     290    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    290291  `).run(
    291292    postId, site.id, finalSlug, req.session.user.id,
    292293    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),
    294295    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    295296    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt,
     
    313314      ActivityPubService.deliverCreate(site, {
    314315        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,
    316317        published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson,
    317318      }).catch(() => { /* best-effort */ });
     
    381382  const cw = (req.body.content_warning || '').trim().slice(0, 200);
    382383  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
    383385  const newSlug = req.body.slug;
    384386  const action = req.body.action || 'save';
     
    424426    UPDATE posts SET
    425427      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 = ?,
    427429      type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, poll_json = ?, publish_at = ?,
    428430      slug = ?, published_at = ?, updated_at = ?
     
    430432  `).run(
    431433    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),
    433435    JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
    434436    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, pollJson, publishAt,
     
    456458    const apPost = {
    457459      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,
    459461      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw, poll_json: pollJson,
    460462    };
Note: See TracChangeset for help on using the changeset viewer.