Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d18c60e0878b426798d32187e85100a2262f1430)
+++ src/services/ActivityPubService.js	(revision 0688b5fcf8935a7e243f429c90946fd67027fb3c)
@@ -400,4 +400,8 @@
   // make it JSON-LD-clean with a context term, otherwise it degrades to the player card.
   if (playable) note.embedUrl = `${base}/embed?post=${encodeURIComponent(post.slug)}`;
+  // Content language → AS2 contentMap (a BCP-47-keyed copy of the content). Mastodon reads the
+  // language from its key for the timeline language filter + the translate button. Emitted
+  // alongside `content` (Mastodon sends both); a plain receiver just uses `content`.
+  if (post.language && /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/.test(post.language)) note.contentMap = { [post.language]: note.content };
   // A hosted poll → federate as an AS2 Question (options + live tally). Do this last so it
   // reuses the note's content/addressing/tags, then swaps the type and strips media.
Index: src/services/Scheduler.js
===================================================================
--- src/services/Scheduler.js	(revision d18c60e0878b426798d32187e85100a2262f1430)
+++ src/services/Scheduler.js	(revision 0688b5fcf8935a7e243f429c90946fd67027fb3c)
@@ -15,5 +15,5 @@
   try {
     const due = db.prepare(`
-      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,
+      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,
              p.published_at, p.publish_at, p.created_at, u.username
       FROM posts p JOIN users u ON u.id = p.author_id
@@ -38,5 +38,5 @@
           ActivityPubService.deliverCreate(site, {
             id: p.id, slug: p.slug, title: p.title || p.slug,
-            content: p.content, cover_image_url: p.cover_image_url || null, cover_video_url: p.cover_video_url || null, cover_alt: p.cover_alt,
+            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,
             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,
           }).catch(() => { /* best-effort */ });
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision d18c60e0878b426798d32187e85100a2262f1430)
+++ src/services/i18n.js	(revision 0688b5fcf8935a7e243f429c90946fd67027fb3c)
@@ -639,4 +639,6 @@
     'pedit.f_cover_alt': 'Alt-tekst (beschrijving)',
     'pedit.cover_alt_placeholder': 'Beschrijf de afbeelding voor schermlezers',
+    'pedit.f_language': 'Taal',
+    'pedit.language_hint': 'voor het fediverse-taalfilter',
     'pedit.cover_upload_btn': 'Upload nieuwe cover',
     'pedit.s_content': 'Content',
@@ -1559,4 +1561,6 @@
     'pedit.f_cover_alt': 'Alt text (description)',
     'pedit.cover_alt_placeholder': 'Describe the image for screen readers',
+    'pedit.f_language': 'Language',
+    'pedit.language_hint': 'for the fediverse language filter',
     'pedit.cover_upload_btn': 'Upload new cover',
     'pedit.s_content': 'Content',
@@ -2479,4 +2483,6 @@
     'pedit.f_cover_alt': 'Alt-Text (Beschreibung)',
     'pedit.cover_alt_placeholder': 'Beschreibe das Bild für Screenreader',
+    'pedit.f_language': 'Sprache',
+    'pedit.language_hint': 'für den Fediverse-Sprachfilter',
     'pedit.cover_upload_btn': 'Neues Titelbild hochladen',
     'pedit.s_content': 'Inhalt',
