ActivityPub Internationalization
When using contentMap, it is generally better not to include content, since the two are functionally redundant. The only real difference is that contentMap carries explicit language tags.
A default language can be specified via the JSON-LD @language context. However, implementations that do not process ActivityPub documents as JSON-LD may ignore this. For that reason, I recommend representing values in contentMap as a single string with an explicit language tag, rather than duplicating content and contentMap.
In theory, when implementing compatibility with plugins like Polylang, contentMap could be used to include multiple language versions. But in the case of WordPress—where articles are the primary content type—this approach may be inefficient. In practice, a more common pattern is to use a single language tag and rely on services like the DeepL API for translation.
Personally, I think multilingual profiles for actors are much more useful than multilingual content objects.
References
- [Activity Streams 2.0 — Natural Language Values](https://www.w3.org/TR/activitystreams-core/#naturalLanguageValues)
A JSON object mapping well-formed [BCP47] Language-Tags to localized, equivalent translations of the same string value. In JSON serialization:
"name","summary","content"→ plain string form"nameMap","summaryMap","contentMap"→ object form with language mappings
- [Activity Streams 2.0 — Default Language Context](https://www.w3.org/TR/activitystreams-core/#defaultlangcontext)
When using [JSON-LD], the
@languageproperty MAY be used to identify the default language. This mechanism may not be understood by implementations that do not process documents as JSON-LD.
(like a multilingual actor profile using nameMap and summaryMap), compare it with a multilingual Article object case
Am I right that you propose to drop the content, name and summary support in favor of the *Map versions?
If so: It would be nice to have a source, why it is recommended to use either or and, because it is a breaking change, a verification that the most used/biggest platforms, to see if they all support the map properties.
Yes, that’s correct — my suggestion is not primarily about compatibility, but rather about efficiency in terms of traffic and data storage, within the range where normal operation can be expected.
Even if we take a conservative approach for maximum compatibility, I think the idea of multilingual profiles is still worth exploring.
But compatibility (or better interoperability) should be our main concern, so even if there are more effective ways to implement features, it is no improvement if federation with other platforms will no longer work.
So we should check other implementations first, before thinking about removing properties.
Have you already had a look into Mastodons or Misskeys code?
But what is missing for an multilingual approach?
I’ll take a closer look after dinner. For Mastodon, I’ve already confirmed that both content and contentMap fields exist together. However, as far as I understand the standard, it doesn’t seem necessary to use both at the same time. I’ll also check how Misskey handles this a bit later.
The provider side is not the important one, we need to know how they parse the Activity and if they support objects without content, but with contentMap instead.
https://github.com/mastodon/mastodon/blob/main/app/lib/activitypub/parser/status_parser.rb#L39
Based on the code snippet you provided, the answer to your question about Mastodon's handling of content versus contentMap is yes, Mastodon's parser supports using contentMap even if the plain content property is absent.
The code confirms that Jiwoon-Kim's proposal is technically feasible for federation with Mastodon.
1. Mastodon Code Analysis: Content Fallback
The logic you found in the ActivityPub::Parser::StatusParser demonstrates a clear fallback mechanism:
def text
if @object['content'].present?
@object['content']
elsif content_language_map?
@object['contentMap'].values.first
end
end
-
Priority 1: The parser first checks for the existence of the plain
@object['content']. If it's present, that value is used as the status text. -
Fallback Logic: If
contentis not present (or blank), the parser checkselsif content_language_map?(which confirms that'contentMap'is a non-empty Hash). If the map exists, it uses@object['contentMap'].values.first, which takes the value of the first language version found in the map.
This confirms that omitting content in favor of contentMap will successfully feed content into Mastodon's status text field.
2. Implication for Efficiency and Interoperability
| Aspect | Jiwoon-Kim's Proposal (contentMap only) |
Status with Mastodon |
|---|---|---|
| Efficiency | ✅ Supported. Eliminates the redundant content string. |
Confirmed. Mastodon's parser handles the missing content. |
| Interoperability (General) | ⚠️ Inconclusive. Still needs verification for other platforms (e.g., Misskey). | Caution remains. While one major platform is confirmed, others might lack this fallback logic. |
3. Final Recommendation
While the technical barrier for Mastodon is removed, the general caution from pfefferle about overall Fediverse interoperability still applies.
-
For your implementation: You are safe to adopt the
contentMaponly approach if you are willing to risk potential compatibility issues with smaller or non-standard compliant servers that lack this specific fallback logic. This is the most efficient path. -
For maximum safety: To guarantee maximum interoperability across the entire Fediverse (including Misskey, Pleroma, etc.), it is still safest to adopt a conservative approach: include both
content(for the primary language) andcontentMap(duplicating the primary content for explicit language tagging) until all major implementations are confirmed to support thecontentMapfallback. -
For Misskey: You still need to perform the code inspection or practical test on Misskey's repository to confirm if it has equivalent fallback logic before fully committing to removing the
contentproperty for your users.
def raw_language_code
if content_language_map?
@object['contentMap'].keys.first
elsif name_language_map?
@object['nameMap'].keys.first
elsif summary_language_map?
@object['summaryMap'].keys.first
end
end
https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/ActivityPubServerService.ts https://github.com/misskey-dev/misskey/tree/develop/packages/backend/test-federation https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/web/FeedService.ts https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/core/activitypub/ApInboxService.ts https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/queue/processors/InboxProcessorService.ts
https://github.com/misskey-dev/misskey/blob/f0fb3a56a8db4c992907f1da026e07b10c4dbd3c/packages/backend/src/core/activitypub/type.ts#L118 export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
https://github.com/misskey-dev/misskey/blob/f0fb3a56a8db4c992907f1da026e07b10c4dbd3c/packages/backend/src/core/activitypub/misc/contexts.ts#L165 'Article': 'as:Article',
https://github.com/misskey-dev/misskey/blob/f0fb3a56a8db4c992907f1da026e07b10c4dbd3c/packages/backend/src/core/activitypub/misc/contexts.ts#L370 'content': 'as:content', 'contentMap': { '@id': 'as:content', '@container': '@language', },
I’m not sure about Misskey, so it seems an actual test will be necessary. I don’t know the exact way to test it myself. At least for Mastodon, support is confirmed.
@pfefferle If there are any other platforms where interoperability should be checked, please let me know. I’m not sure if I can easily find the relevant source code, but I’ll give it a try. In fact, the simplest and most reliable way might be to just create a test object, federate it, and inspect the proxy objects.
- https://git.pleroma.social/mjc1/pleroma/-/blob/v0.9.9999/test/fixtures/mastodon-post-activity-contentmap.json?utm_source=chatgpt.com
- https://s3lph.me/activitypub-static-site.html?utm_source=chatgpt.com
https://mastodon.social/@thaumiel999/115439707686351557