diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs
index 8e5a4175..764c5dc7 100644
--- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs
+++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Newtonsoft.Json;
@@ -6,10 +7,113 @@ using J = Newtonsoft.Json.JsonPropertyAttribute;
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
+public class LDLocalizedString
+{
+ ///
+ /// key: BCP 47 language code, with empty string meaning "unknown"
+ /// value: content, in that language
+ ///
+ public Dictionary Values { get; set; }
+
+ public LDLocalizedString()
+ {
+ Values = [];
+ }
+
+ public LDLocalizedString(string? language, string? value)
+ {
+ Values = [];
+
+ // this is required to create a non-Map field for non-JsonLD remotes.
+ Values.Add("", value);
+
+ if (language != null)
+ {
+ language = NormalizeLanguageCode(language);
+
+ if (language != null && language != "")
+ Values.Add(language, value);
+ }
+ }
+
+ ///
+ /// If the remote sends different content to multiple languages, try and guess which one they prefer "by default".
+ ///
+ /// This idea was taken from Sharkey's implementation.
+ /// https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/401#note_1174
+ ///
+ /// The guessed language
+ /// The value, in the guessed language
+ public string? GuessPreferredValue(out string? language)
+ {
+ if (Values.Count > 1)
+ {
+ var unknownValue = GetUnknownValue();
+ if (unknownValue != null)
+ {
+ var preferred = Values.FirstOrDefault(i => !IsUnknownLanguage(i.Key) && i.Value == unknownValue);
+ if (preferred.Value != null)
+ {
+ language = NormalizeLanguageCode(preferred.Key);
+ return preferred.Value;
+ }
+ }
+ }
+ else if (Values.Count == 0)
+ {
+ language = null;
+ return null;
+ }
+
+ var first = Values.FirstOrDefault(i => !IsUnknownLanguage(i.Key));
+ if (first.Value == null)
+ {
+ first = Values.FirstOrDefault();
+ if (first.Value == null)
+ {
+ language = null;
+ return null;
+ }
+ }
+
+ language = IsUnknownLanguage(first.Key) ? null : NormalizeLanguageCode(first.Key);
+ return first.Value;
+ }
+
+ public static string? NormalizeLanguageCode(string lang)
+ {
+ try
+ {
+ return CultureInfo.GetCultureInfo(lang).ToString();
+ }
+ catch (CultureNotFoundException)
+ {
+ // invalid language code
+ return null;
+ }
+ }
+
+ // Akkoma forces all non-localized text to be in the "und" language by adding { "@language":"und" } to it's context
+ public static bool IsUnknownLanguage(string? lang) => lang == null || lang == "" || lang == "und";
+ public string? GetUnknownValue()
+ {
+ string? value;
+
+ if (Values.TryGetValue("", out value))
+ return value;
+
+ if (Values.TryGetValue("und", out value))
+ return value;
+
+ return null;
+ }
+}
+
public class LDValueObject
{
- [J("@type")] public string? Type { get; set; }
- [J("@value")] public required T Value { get; set; }
+ [J("@type")] public string? Type { get; set; }
+ [J("@value")] public required T Value { get; set; }
+ [J("@language")] public string? Language { get; set; }
}
public class ValueObjectConverter : JsonConverter
@@ -157,4 +261,53 @@ public sealed class XsdString(string? str)
public static implicit operator string?(XsdString? s) => s?.Str;
public override string? ToString() => str;
+}
+
+public class LocalizedValueObjectConverter : JsonConverter
+{
+ public override LDLocalizedString? ReadJson(JsonReader reader, Type objectType, LDLocalizedString? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var obj = JArray.Load(reader);
+ var list = obj.ToObject>>();
+ if (list == null || list.Count == 0)
+ return null;
+
+ var localized = new LDLocalizedString();
+
+ foreach (var item in list)
+ {
+ localized.Values.Add(item.Language ?? "", item.Value);
+ }
+
+ return localized;
+ }
+
+ public override void WriteJson(JsonWriter writer, LDLocalizedString? value, JsonSerializer serializer)
+ {
+ if (value == null)
+ {
+ writer.WriteNull();
+ return;
+ }
+
+ writer.WriteStartArray();
+
+ foreach (var item in value.Values)
+ {
+ writer.WriteStartObject();
+
+ if (!LDLocalizedString.IsUnknownLanguage(item.Key))
+ {
+ writer.WritePropertyName("@language");
+ writer.WriteValue(item.Key);
+ }
+
+ writer.WritePropertyName("@value");
+ writer.WriteValue(item.Value);
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+ }
}
\ No newline at end of file