@fedify/vocab 2.4.0-pr.936.41 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/mod.cjs +32014 -9323
- package/dist/mod.d.cts +4770 -91
- package/dist/mod.d.ts +4770 -91
- package/dist/mod.js +31907 -9223
- package/dist-tests/actor-D_QaCMro.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-BevjwZhB.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
- package/dist-tests/vocab.test.mjs +901 -11
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +1219 -5
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { test } from "@fedify/fixture";
|
|
2
|
+
import {
|
|
3
|
+
type DocumentLoader,
|
|
4
|
+
getDocumentLoader,
|
|
5
|
+
preloadedContexts,
|
|
6
|
+
} from "@fedify/vocab-runtime";
|
|
7
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
8
|
+
import {
|
|
9
|
+
Application,
|
|
10
|
+
Article,
|
|
11
|
+
Collection,
|
|
12
|
+
Create,
|
|
13
|
+
Group,
|
|
14
|
+
LanguageString,
|
|
15
|
+
Link,
|
|
16
|
+
Note,
|
|
17
|
+
Object as ASObject,
|
|
18
|
+
Organization,
|
|
19
|
+
Person,
|
|
20
|
+
PropertyValue,
|
|
21
|
+
Service,
|
|
22
|
+
Translation,
|
|
23
|
+
Update,
|
|
24
|
+
} from "./mod.ts";
|
|
25
|
+
|
|
26
|
+
// Keep this binding separate from Temporal injected into bundled tests.
|
|
27
|
+
const { Temporal: TemporalPolyfill } = await import("temporal-polyfill");
|
|
28
|
+
|
|
29
|
+
// FEP-22cd draft and examples, pinned to:
|
|
30
|
+
// https://codeberg.org/fediverse/fep/src/commit/6d0d6559054baeb7b71a5f2bdc14fc4c09b66f39/fep/22cd/fep-22cd.md
|
|
31
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
32
|
+
const FEP = "https://w3id.org/fep/22cd";
|
|
33
|
+
const SCHEMA = "https://schema.org/";
|
|
34
|
+
const articleId = new URL("https://example.com/articles/1");
|
|
35
|
+
const alice = new URL("https://example.com/users/alice");
|
|
36
|
+
const context = [AS, FEP];
|
|
37
|
+
const sourceUpdated = TemporalPolyfill.Instant.from("2026-09-01T00:00:00Z");
|
|
38
|
+
const entry = {
|
|
39
|
+
type: "Translation",
|
|
40
|
+
inLanguage: "ko",
|
|
41
|
+
url: "https://example.com/articles/1/ko",
|
|
42
|
+
translator: [alice.href],
|
|
43
|
+
translationOfWork: articleId.href,
|
|
44
|
+
sourceUpdated: sourceUpdated.toString(),
|
|
45
|
+
};
|
|
46
|
+
const example = {
|
|
47
|
+
"@context": context,
|
|
48
|
+
id: articleId.href,
|
|
49
|
+
type: "Article",
|
|
50
|
+
attributedTo: alice.href,
|
|
51
|
+
updated: sourceUpdated.toString(),
|
|
52
|
+
contentMap: { en: "<p>Original text...</p>", ko: "<p>번역 텍스트...</p>" },
|
|
53
|
+
translations: [entry],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function translation(): Translation {
|
|
57
|
+
return new Translation({
|
|
58
|
+
language: new Intl.Locale("ko"),
|
|
59
|
+
translator: alice,
|
|
60
|
+
original: articleId,
|
|
61
|
+
sourceUpdated,
|
|
62
|
+
url: new URL(entry.url),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function article(): Article {
|
|
67
|
+
return new Article({
|
|
68
|
+
id: articleId,
|
|
69
|
+
attribution: alice,
|
|
70
|
+
updated: sourceUpdated,
|
|
71
|
+
contents: [
|
|
72
|
+
new LanguageString(example.contentMap.en, "en"),
|
|
73
|
+
new LanguageString(example.contentMap.ko, "ko"),
|
|
74
|
+
],
|
|
75
|
+
translations: [translation()],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function record(value: unknown): Record<string, unknown> {
|
|
80
|
+
ok(value != null && typeof value === "object" && !Array.isArray(value));
|
|
81
|
+
return value as Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const noFetch: DocumentLoader = (url) => {
|
|
85
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function actorLoader(type: string, id?: string): DocumentLoader {
|
|
89
|
+
return (url) =>
|
|
90
|
+
Promise.resolve({
|
|
91
|
+
contextUrl: null,
|
|
92
|
+
documentUrl: url,
|
|
93
|
+
document: { "@context": AS, type, id: id ?? url },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("FEP-22cd canonical context is preloaded", async () => {
|
|
98
|
+
ok(FEP in preloadedContexts);
|
|
99
|
+
const loaded = await getDocumentLoader()(FEP);
|
|
100
|
+
deepStrictEqual(record(loaded.document)["@context"], {
|
|
101
|
+
"fep-22cd": `${FEP}#`,
|
|
102
|
+
schema: SCHEMA,
|
|
103
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
104
|
+
translations: { "@id": "fep-22cd:translations", "@container": "@set" },
|
|
105
|
+
Translation: "fep-22cd:Translation",
|
|
106
|
+
sourceUpdated: { "@id": "fep-22cd:sourceUpdated", "@type": "xsd:dateTime" },
|
|
107
|
+
translator: {
|
|
108
|
+
"@id": "schema:translator",
|
|
109
|
+
"@type": "@id",
|
|
110
|
+
"@container": "@set",
|
|
111
|
+
},
|
|
112
|
+
inLanguage: "schema:inLanguage",
|
|
113
|
+
translationOfWork: { "@id": "schema:translationOfWork", "@type": "@id" },
|
|
114
|
+
isBasedOn: { "@id": "schema:isBasedOn", "@type": "@id" },
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("FEP-22cd personal translation compact and expanded round trips", async () => {
|
|
119
|
+
for (const format of [undefined, "compact", "expand"] as const) {
|
|
120
|
+
const encoded = await article().toJsonLd({ format });
|
|
121
|
+
if (format !== "expand") {
|
|
122
|
+
deepStrictEqual(record(encoded).translations, [entry]);
|
|
123
|
+
ok((record(encoded)["@context"] as unknown[]).includes(FEP));
|
|
124
|
+
}
|
|
125
|
+
const restored = await Article.fromJsonLd(encoded, {
|
|
126
|
+
documentLoader: noFetch,
|
|
127
|
+
});
|
|
128
|
+
deepStrictEqual(restored.attributionId, alice);
|
|
129
|
+
deepStrictEqual(restored.contents, article().contents);
|
|
130
|
+
deepStrictEqual(restored.translations.length, 1);
|
|
131
|
+
const t = restored.translations[0];
|
|
132
|
+
deepStrictEqual(t.language?.baseName, "ko");
|
|
133
|
+
deepStrictEqual(t.original, articleId);
|
|
134
|
+
deepStrictEqual(t.translatorIds, [alice]);
|
|
135
|
+
deepStrictEqual(t.sourceUpdated?.toString(), sourceUpdated.toString());
|
|
136
|
+
deepStrictEqual(t.url, new URL(entry.url));
|
|
137
|
+
// Explicit compact bypasses the input cache.
|
|
138
|
+
deepStrictEqual(
|
|
139
|
+
record(await restored.toJsonLd({ format: "compact" })).translations,
|
|
140
|
+
[entry],
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const restored = await Article.fromJsonLd(example, {
|
|
144
|
+
documentLoader: noFetch,
|
|
145
|
+
});
|
|
146
|
+
deepStrictEqual(
|
|
147
|
+
record(await restored.toJsonLd({ format: "compact", context }))
|
|
148
|
+
.translations,
|
|
149
|
+
[entry],
|
|
150
|
+
);
|
|
151
|
+
deepStrictEqual(
|
|
152
|
+
await article().toJsonLd({ format: "compact", context }),
|
|
153
|
+
example,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("FEP-22cd full IRIs, optional revision, script language and AS links", async () => {
|
|
158
|
+
const expanded = {
|
|
159
|
+
"@type": `${FEP}#Translation`,
|
|
160
|
+
[`${SCHEMA}inLanguage`]: "zh-Hant",
|
|
161
|
+
[`${SCHEMA}translator`]: [{ "@id": alice.href }],
|
|
162
|
+
[`${SCHEMA}translationOfWork`]: { "@id": articleId.href },
|
|
163
|
+
[`${SCHEMA}isBasedOn`]: { "@id": `${articleId.href}/revisions/2` },
|
|
164
|
+
[`${AS}#url`]: [
|
|
165
|
+
{ "@id": `${articleId.href}/zh-Hant` },
|
|
166
|
+
{
|
|
167
|
+
"@type": `${AS}#Link`,
|
|
168
|
+
[`${AS}#href`]: { "@id": `${articleId.href}/zh-Hant.html` },
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
const t = await Translation.fromJsonLd(expanded, { documentLoader: noFetch });
|
|
173
|
+
deepStrictEqual(t.language?.baseName, "zh-Hant");
|
|
174
|
+
deepStrictEqual(t.original, articleId);
|
|
175
|
+
deepStrictEqual(t.basis?.href, `${articleId.href}/revisions/2`);
|
|
176
|
+
deepStrictEqual(t.sourceUpdated, null);
|
|
177
|
+
ok(t.urls[1] instanceof Link);
|
|
178
|
+
const compact = record(await t.toJsonLd({ format: "compact" }));
|
|
179
|
+
deepStrictEqual(compact.translator, [alice.href]);
|
|
180
|
+
deepStrictEqual(compact.inLanguage, "zh-Hant");
|
|
181
|
+
deepStrictEqual(compact.isBasedOn, `${articleId.href}/revisions/2`);
|
|
182
|
+
ok(!("sourceUpdated" in compact));
|
|
183
|
+
ok(Object.keys(compact).every((key) => !key.startsWith("https:")));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("FEP-22cd organization languages, multiple translators and review-only Update", async () => {
|
|
187
|
+
const initial = article().clone({
|
|
188
|
+
attribution: new URL("https://example.com/orgs/acme"),
|
|
189
|
+
names: [
|
|
190
|
+
new LanguageString("Title", "en"),
|
|
191
|
+
new LanguageString("제목", "ko"),
|
|
192
|
+
],
|
|
193
|
+
summaries: [
|
|
194
|
+
new LanguageString("Summary", "en"),
|
|
195
|
+
new LanguageString("요약", "ko"),
|
|
196
|
+
],
|
|
197
|
+
contents: [
|
|
198
|
+
...article().contents,
|
|
199
|
+
new LanguageString("日本語版", "ja"),
|
|
200
|
+
"Fallback",
|
|
201
|
+
],
|
|
202
|
+
translations: [
|
|
203
|
+
translation(),
|
|
204
|
+
new Translation({
|
|
205
|
+
language: new Intl.Locale("ja"),
|
|
206
|
+
translators: [
|
|
207
|
+
new URL("https://example.com/users/bob"),
|
|
208
|
+
new URL("https://example.com/actors/translator"),
|
|
209
|
+
],
|
|
210
|
+
original: articleId,
|
|
211
|
+
}),
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
const editedAt = TemporalPolyfill.Instant.from("2026-09-15T00:00:00Z");
|
|
215
|
+
const edited = initial.clone({ updated: editedAt });
|
|
216
|
+
ok(
|
|
217
|
+
TemporalPolyfill.Instant.compare(
|
|
218
|
+
edited.translations[0].sourceUpdated!,
|
|
219
|
+
edited.updated!,
|
|
220
|
+
) < 0,
|
|
221
|
+
);
|
|
222
|
+
const reviewed = edited.clone({
|
|
223
|
+
translations: [
|
|
224
|
+
edited.translations[0].clone({ sourceUpdated: edited.updated }),
|
|
225
|
+
edited.translations[1],
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
const update = new Update({
|
|
229
|
+
id: new URL(`${articleId.href}/review`),
|
|
230
|
+
actor: alice,
|
|
231
|
+
object: reviewed,
|
|
232
|
+
});
|
|
233
|
+
const restored = await Update.fromJsonLd(
|
|
234
|
+
await update.toJsonLd({ format: "compact" }),
|
|
235
|
+
{ documentLoader: noFetch },
|
|
236
|
+
);
|
|
237
|
+
const object = await restored.getObject();
|
|
238
|
+
ok(object instanceof Article);
|
|
239
|
+
deepStrictEqual(object.id, articleId);
|
|
240
|
+
const byLanguage = (values: readonly (string | LanguageString)[]) =>
|
|
241
|
+
Object.fromEntries(values.map((value) => [
|
|
242
|
+
value instanceof LanguageString ? value.locale.baseName : "@none",
|
|
243
|
+
value.toString(),
|
|
244
|
+
]));
|
|
245
|
+
deepStrictEqual(byLanguage(object.contents), byLanguage(initial.contents));
|
|
246
|
+
deepStrictEqual(object.names, initial.names);
|
|
247
|
+
deepStrictEqual(object.summaries, initial.summaries);
|
|
248
|
+
deepStrictEqual(object.attributionId, initial.attributionId);
|
|
249
|
+
deepStrictEqual(object.translations.map((t) => t.language?.baseName), [
|
|
250
|
+
"ko",
|
|
251
|
+
"ja",
|
|
252
|
+
]);
|
|
253
|
+
deepStrictEqual(
|
|
254
|
+
object.translations[0].sourceUpdated?.toString(),
|
|
255
|
+
editedAt.toString(),
|
|
256
|
+
);
|
|
257
|
+
deepStrictEqual(object.translations[1].sourceUpdated, null);
|
|
258
|
+
deepStrictEqual(
|
|
259
|
+
object.translations[1].translatorIds,
|
|
260
|
+
initial.translations[1].translatorIds,
|
|
261
|
+
);
|
|
262
|
+
const withdrawn = reviewed.clone({
|
|
263
|
+
translations: [],
|
|
264
|
+
contents: [new LanguageString("Original", "en")],
|
|
265
|
+
});
|
|
266
|
+
const replaced = await Article.fromJsonLd(await withdrawn.toJsonLd());
|
|
267
|
+
deepStrictEqual(replaced.translations, []);
|
|
268
|
+
deepStrictEqual(replaced.id, articleId);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("FEP-22cd nested fresh and cached objects compact without full-IRI terms", async () => {
|
|
272
|
+
for (const child of [article(), await Article.fromJsonLd(example)]) {
|
|
273
|
+
for (
|
|
274
|
+
const wrapper of [
|
|
275
|
+
new Create({ object: child }),
|
|
276
|
+
new Collection({ items: [child] }),
|
|
277
|
+
]
|
|
278
|
+
) {
|
|
279
|
+
for (const format of [undefined, "compact"] as const) {
|
|
280
|
+
const json = record(await wrapper.toJsonLd({ format }));
|
|
281
|
+
const nested = record(json.object ?? json.items);
|
|
282
|
+
deepStrictEqual(nested.translations, [entry]);
|
|
283
|
+
const roundTrip = await ASObject.fromJsonLd(json);
|
|
284
|
+
ok(roundTrip instanceof Create || roundTrip instanceof Collection);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const custom = record(
|
|
289
|
+
await article().toJsonLd({ format: "compact", context: AS }),
|
|
290
|
+
);
|
|
291
|
+
deepStrictEqual(custom["@context"], AS);
|
|
292
|
+
ok(`${FEP}#translations` in custom);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("FEP-22cd all actor types use the existing linked-object getters", async () => {
|
|
296
|
+
for (const Type of [Person, Organization, Group, Application, Service]) {
|
|
297
|
+
const t = new Translation({ translator: new Type({ id: alice }) });
|
|
298
|
+
ok(await t.getTranslator({ documentLoader: noFetch }) instanceof Type);
|
|
299
|
+
const parsed = await Translation.fromJsonLd(await t.toJsonLd());
|
|
300
|
+
ok(
|
|
301
|
+
await parsed.getTranslator({
|
|
302
|
+
documentLoader: actorLoader(Type.name),
|
|
303
|
+
}) instanceof Type,
|
|
304
|
+
);
|
|
305
|
+
deepStrictEqual((await Array.fromAsync(parsed.getTranslators())).length, 1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("FEP-22cd metadata ids are not fetched or trusted as actor origins", async () => {
|
|
310
|
+
for (const id of [undefined, "https://victim.example/metadata"]) {
|
|
311
|
+
for (const plural of [false, true]) {
|
|
312
|
+
const t = await Translation.fromJsonLd({
|
|
313
|
+
"@context": context,
|
|
314
|
+
type: "Translation",
|
|
315
|
+
...(id == null ? {} : { id }),
|
|
316
|
+
translator: [{
|
|
317
|
+
id: "https://victim.example/actor",
|
|
318
|
+
type: "Application",
|
|
319
|
+
}],
|
|
320
|
+
});
|
|
321
|
+
const fetched: string[] = [];
|
|
322
|
+
const documentLoader: DocumentLoader = (url) => {
|
|
323
|
+
fetched.push(url);
|
|
324
|
+
return actorLoader("Person")(url);
|
|
325
|
+
};
|
|
326
|
+
const actors = plural
|
|
327
|
+
? await Array.fromAsync(t.getTranslators({ documentLoader }))
|
|
328
|
+
: [await t.getTranslator({ documentLoader })];
|
|
329
|
+
deepStrictEqual(fetched, ["https://victim.example/actor"]);
|
|
330
|
+
ok(actors[0] instanceof Person);
|
|
331
|
+
deepStrictEqual(t.id?.href, id);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const parsed = await Article.fromJsonLd({
|
|
335
|
+
...example,
|
|
336
|
+
translations: [{ ...entry, id: "urn:translation:ko" }],
|
|
337
|
+
}, { documentLoader: noFetch });
|
|
338
|
+
deepStrictEqual(parsed.translations[0].id?.href, "urn:translation:ko");
|
|
339
|
+
await parsed.toJsonLd({ format: "compact" });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Mirrors the documentation's completeness check before classifying credit.
|
|
343
|
+
async function hasCompleteCredit(
|
|
344
|
+
t: Translation,
|
|
345
|
+
documentLoader: DocumentLoader,
|
|
346
|
+
): Promise<boolean> {
|
|
347
|
+
const ids = t.translatorIds;
|
|
348
|
+
const actors = await Array.fromAsync(
|
|
349
|
+
t.getTranslators({ documentLoader, suppressError: true }),
|
|
350
|
+
);
|
|
351
|
+
return actors.length > 0 && actors.length >= ids.length &&
|
|
352
|
+
actors.every((actor) => actor.id != null);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test("FEP-22cd unavailable, deleted, mismatched and id-less actors leave status unknown", async () => {
|
|
356
|
+
for (
|
|
357
|
+
const failedLoader of [
|
|
358
|
+
noFetch,
|
|
359
|
+
actorLoader("Tombstone"),
|
|
360
|
+
actorLoader("Person", "https://spoof.example/actor"),
|
|
361
|
+
]
|
|
362
|
+
) {
|
|
363
|
+
const t = new Translation({
|
|
364
|
+
translators: [
|
|
365
|
+
alice,
|
|
366
|
+
new Application({ id: new URL("https://example.com/machine") }),
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
deepStrictEqual(await hasCompleteCredit(t, failedLoader), false);
|
|
370
|
+
deepStrictEqual(t.translatorIds, [
|
|
371
|
+
alice,
|
|
372
|
+
new URL("https://example.com/machine"),
|
|
373
|
+
]);
|
|
374
|
+
deepStrictEqual(
|
|
375
|
+
record(await t.toJsonLd({ format: "compact" })).translator instanceof
|
|
376
|
+
Array,
|
|
377
|
+
true,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const idless = await Translation.fromJsonLd({
|
|
381
|
+
"@context": context,
|
|
382
|
+
type: "Translation",
|
|
383
|
+
translator: [alice.href, { type: "Application" }],
|
|
384
|
+
});
|
|
385
|
+
deepStrictEqual(
|
|
386
|
+
await hasCompleteCredit(idless, actorLoader("Person")),
|
|
387
|
+
false,
|
|
388
|
+
);
|
|
389
|
+
await rejects(
|
|
390
|
+
() =>
|
|
391
|
+
new Translation({ translator: alice }).getTranslator({
|
|
392
|
+
documentLoader: noFetch,
|
|
393
|
+
}),
|
|
394
|
+
/Unexpected fetch/,
|
|
395
|
+
);
|
|
396
|
+
const t = new Translation({ translator: alice });
|
|
397
|
+
await rejects(() =>
|
|
398
|
+
t.getTranslator({
|
|
399
|
+
documentLoader: actorLoader("Person", "https://spoof.example/actor"),
|
|
400
|
+
crossOrigin: "throw",
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("FEP-22cd malformed metadata follows vocabulary parsing behavior", async () => {
|
|
406
|
+
await rejects(
|
|
407
|
+
() =>
|
|
408
|
+
Article.fromJsonLd({
|
|
409
|
+
...example,
|
|
410
|
+
translations: [{ ...entry, inLanguage: "not_a_language" }],
|
|
411
|
+
}),
|
|
412
|
+
RangeError,
|
|
413
|
+
);
|
|
414
|
+
await rejects(
|
|
415
|
+
() =>
|
|
416
|
+
Article.fromJsonLd({
|
|
417
|
+
...example,
|
|
418
|
+
translations: [{ ...entry, sourceUpdated: "2026-09-01" }],
|
|
419
|
+
}),
|
|
420
|
+
RangeError,
|
|
421
|
+
);
|
|
422
|
+
await rejects(
|
|
423
|
+
() =>
|
|
424
|
+
Article.fromJsonLd({
|
|
425
|
+
...example,
|
|
426
|
+
translations: [{ ...entry, type: "Note" }],
|
|
427
|
+
}),
|
|
428
|
+
TypeError,
|
|
429
|
+
);
|
|
430
|
+
// As with other value types, required FEP fields are not enforced by the parser.
|
|
431
|
+
const idOnly = await Article.fromJsonLd({
|
|
432
|
+
...example,
|
|
433
|
+
translations: [{ id: "urn:translation:ko" }],
|
|
434
|
+
}, { documentLoader: noFetch });
|
|
435
|
+
deepStrictEqual(idOnly.translations[0].id?.href, "urn:translation:ko");
|
|
436
|
+
deepStrictEqual(idOnly.translations[0].language, null);
|
|
437
|
+
deepStrictEqual(idOnly.translations[0].translatorIds, []);
|
|
438
|
+
const missing = await Article.fromJsonLd({
|
|
439
|
+
...example,
|
|
440
|
+
translations: undefined,
|
|
441
|
+
});
|
|
442
|
+
deepStrictEqual(missing.translations, []);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("FEP-22cd ordinary multilingual serialization remains unchanged", async () => {
|
|
446
|
+
const fields = {
|
|
447
|
+
id: articleId,
|
|
448
|
+
attribution: alice,
|
|
449
|
+
contents: [
|
|
450
|
+
"Fallback",
|
|
451
|
+
new LanguageString("Original", "en"),
|
|
452
|
+
new LanguageString("번역", "ko"),
|
|
453
|
+
],
|
|
454
|
+
names: [
|
|
455
|
+
new LanguageString("Title", "en"),
|
|
456
|
+
new LanguageString("제목", "ko"),
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
for (const Type of [Article, Note, ASObject]) {
|
|
460
|
+
for (const format of [undefined, "compact"] as const) {
|
|
461
|
+
const json = record(await new Type(fields).toJsonLd({ format }));
|
|
462
|
+
ok(!JSON.stringify(json["@context"]).includes(FEP));
|
|
463
|
+
deepStrictEqual(json.contentMap, { en: "Original", ko: "번역" });
|
|
464
|
+
deepStrictEqual(json.content, "Fallback");
|
|
465
|
+
deepStrictEqual(json.nameMap, { en: "Title", ko: "제목" });
|
|
466
|
+
deepStrictEqual(json.attributedTo, alice.href);
|
|
467
|
+
ok(!("translations" in json));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("FEP-22cd keeps Mastodon PropertyValue and Schema.org translation namespaces distinct", async () => {
|
|
473
|
+
const person = new Person({
|
|
474
|
+
id: alice,
|
|
475
|
+
attachments: [
|
|
476
|
+
new PropertyValue({ name: "Website", value: "https://example.com/" }),
|
|
477
|
+
],
|
|
478
|
+
translations: [translation()],
|
|
479
|
+
});
|
|
480
|
+
const json = record(await person.toJsonLd());
|
|
481
|
+
deepStrictEqual(json.attachment, {
|
|
482
|
+
type: "PropertyValue",
|
|
483
|
+
name: "Website",
|
|
484
|
+
value: "https://example.com/",
|
|
485
|
+
});
|
|
486
|
+
deepStrictEqual(json.translations, [entry]);
|
|
487
|
+
const restored = await Person.fromJsonLd(json);
|
|
488
|
+
const attachments = await Array.fromAsync(restored.getAttachments());
|
|
489
|
+
ok(attachments[0] instanceof PropertyValue);
|
|
490
|
+
deepStrictEqual(attachments[0].value, "https://example.com/");
|
|
491
|
+
deepStrictEqual(restored.translations[0].original, articleId);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("FEP-22cd explicit actor trust remains opt-in and clones retain untrusted state", async () => {
|
|
495
|
+
const parsed = await Translation.fromJsonLd({
|
|
496
|
+
"@context": context,
|
|
497
|
+
type: "Translation",
|
|
498
|
+
id: "https://victim.example/metadata",
|
|
499
|
+
translator: [{ id: "https://victim.example/actor", type: "Application" }],
|
|
500
|
+
});
|
|
501
|
+
ok(
|
|
502
|
+
await parsed.getTranslator({
|
|
503
|
+
documentLoader: noFetch,
|
|
504
|
+
crossOrigin: "trust",
|
|
505
|
+
}) instanceof Application,
|
|
506
|
+
);
|
|
507
|
+
const clone = parsed.clone({ sourceUpdated });
|
|
508
|
+
ok(
|
|
509
|
+
await clone.getTranslator({
|
|
510
|
+
documentLoader: actorLoader("Person"),
|
|
511
|
+
}) instanceof Person,
|
|
512
|
+
);
|
|
513
|
+
const local = parsed.clone({ translator: new Application({ id: alice }) });
|
|
514
|
+
ok(
|
|
515
|
+
await local.getTranslator({ documentLoader: noFetch }) instanceof
|
|
516
|
+
Application,
|
|
517
|
+
);
|
|
518
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
$schema: ../../vocab-tools/schema.yaml
|
|
2
|
+
name: Translation
|
|
3
|
+
compactName: Translation
|
|
4
|
+
uri: "https://w3id.org/fep/22cd#Translation"
|
|
5
|
+
entity: false
|
|
6
|
+
trustEmbeddedObjects: false
|
|
7
|
+
description: |
|
|
8
|
+
Translator credit and source freshness for one language version of an
|
|
9
|
+
{@link Object}, as defined by the draft
|
|
10
|
+
[FEP-22cd](https://w3id.org/fep/22cd). This is embedded metadata, not an
|
|
11
|
+
independent post or activity target. Its optional identifier need not be
|
|
12
|
+
dereferenceable and does not establish trust in embedded translator actors.
|
|
13
|
+
|
|
14
|
+
Targets [draft revision 6d0d6559](https://codeberg.org/fediverse/fep/src/commit/6d0d6559054baeb7b71a5f2bdc14fc4c09b66f39/fep/22cd/fep-22cd.md).
|
|
15
|
+
defaultContext:
|
|
16
|
+
- "https://www.w3.org/ns/activitystreams"
|
|
17
|
+
- "https://w3id.org/fep/22cd"
|
|
18
|
+
properties:
|
|
19
|
+
- singularName: language
|
|
20
|
+
functional: true
|
|
21
|
+
compactName: inLanguage
|
|
22
|
+
uri: "https://schema.org/inLanguage"
|
|
23
|
+
description: |
|
|
24
|
+
The translated language. Publishers must match a language in the
|
|
25
|
+
containing object's content, name, or summary map.
|
|
26
|
+
range:
|
|
27
|
+
- "fedify:langTag"
|
|
28
|
+
|
|
29
|
+
- pluralName: translators
|
|
30
|
+
singularName: translator
|
|
31
|
+
singularAccessor: true
|
|
32
|
+
compactName: translator
|
|
33
|
+
uri: "https://schema.org/translator"
|
|
34
|
+
extraContext: "https://w3id.org/fep/22cd"
|
|
35
|
+
description: |
|
|
36
|
+
The actors credited for this language version. Their order has no
|
|
37
|
+
meaning. Human actors credit translation or review; Application and
|
|
38
|
+
Service actors credit machine output. If any actor cannot be resolved,
|
|
39
|
+
the entry's human-or-machine status is unknown. Credit is asserted by
|
|
40
|
+
the publisher and grants no publishing authority. Embedded actors with
|
|
41
|
+
identifiers are fetched before being returned unless explicitly trusted.
|
|
42
|
+
range:
|
|
43
|
+
- "https://www.w3.org/ns/activitystreams#Application"
|
|
44
|
+
- "https://www.w3.org/ns/activitystreams#Group"
|
|
45
|
+
- "https://www.w3.org/ns/activitystreams#Organization"
|
|
46
|
+
- "https://www.w3.org/ns/activitystreams#Person"
|
|
47
|
+
- "https://www.w3.org/ns/activitystreams#Service"
|
|
48
|
+
|
|
49
|
+
- singularName: original
|
|
50
|
+
functional: true
|
|
51
|
+
compactName: translationOfWork
|
|
52
|
+
uri: "https://schema.org/translationOfWork"
|
|
53
|
+
description: |
|
|
54
|
+
The identifier of the containing object. FEP-22cd translations share
|
|
55
|
+
that object's identity rather than introducing separate posts.
|
|
56
|
+
range:
|
|
57
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
58
|
+
|
|
59
|
+
- singularName: sourceUpdated
|
|
60
|
+
functional: true
|
|
61
|
+
compactName: sourceUpdated
|
|
62
|
+
uri: "https://w3id.org/fep/22cd#sourceUpdated"
|
|
63
|
+
description: |
|
|
64
|
+
The source object's updated timestamp (or published timestamp if it has
|
|
65
|
+
no updated timestamp) at the last human review of this translation.
|
|
66
|
+
An absent value makes no freshness claim. A value earlier than the
|
|
67
|
+
current source timestamp indicates a potentially stale translation;
|
|
68
|
+
an equal or later value reflects the current source.
|
|
69
|
+
range:
|
|
70
|
+
- "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
71
|
+
|
|
72
|
+
- singularName: basis
|
|
73
|
+
functional: true
|
|
74
|
+
compactName: isBasedOn
|
|
75
|
+
uri: "https://schema.org/isBasedOn"
|
|
76
|
+
description: |
|
|
77
|
+
An optional public revision resource identifying the source reviewed.
|
|
78
|
+
Supplements sourceUpdated without requiring private revision history
|
|
79
|
+
to be exposed.
|
|
80
|
+
range:
|
|
81
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
82
|
+
|
|
83
|
+
- pluralName: urls
|
|
84
|
+
singularName: url
|
|
85
|
+
singularAccessor: true
|
|
86
|
+
compactName: url
|
|
87
|
+
uri: "https://www.w3.org/ns/activitystreams#url"
|
|
88
|
+
description: Links to renderings of this specific language version.
|
|
89
|
+
range:
|
|
90
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
91
|
+
- "https://www.w3.org/ns/activitystreams#Link"
|