@fedify/vocab 2.4.0-dev.1962 → 2.4.0-dev.1964

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.
@@ -0,0 +1,404 @@
1
+ import "temporal-polyfill";
2
+ globalThis.addEventListener = () => {};
3
+ import { F as PropertyValue, G as Update, M as Organization, N as Person, O as Note, T as Link, V as Service, W as Translation, a as Application, k as Object$1, l as Create, o as Article, s as Collection, y as Group } from "./vocab-D3s0_CFf.mjs";
4
+ import "./actor-Bms47rRP.mjs";
5
+ import "./lookup-DaLkzo2s.mjs";
6
+ import { test } from "@fedify/fixture";
7
+ import { deepStrictEqual, ok, rejects } from "node:assert/strict";
8
+ import { LanguageString as LanguageString$1, getDocumentLoader, preloadedContexts } from "@fedify/vocab-runtime";
9
+ new URL("https://www.w3.org/ns/activitystreams#Public");
10
+ //#endregion
11
+ //#region src/translation.test.ts
12
+ const { Temporal: TemporalPolyfill } = await import("temporal-polyfill");
13
+ const AS = "https://www.w3.org/ns/activitystreams";
14
+ const FEP = "https://w3id.org/fep/22cd";
15
+ const SCHEMA = "https://schema.org/";
16
+ const articleId = new URL("https://example.com/articles/1");
17
+ const alice = new URL("https://example.com/users/alice");
18
+ const context = [AS, FEP];
19
+ const sourceUpdated = TemporalPolyfill.Instant.from("2026-09-01T00:00:00Z");
20
+ const entry = {
21
+ type: "Translation",
22
+ inLanguage: "ko",
23
+ url: "https://example.com/articles/1/ko",
24
+ translator: [alice.href],
25
+ translationOfWork: articleId.href,
26
+ sourceUpdated: sourceUpdated.toString()
27
+ };
28
+ const example = {
29
+ "@context": context,
30
+ id: articleId.href,
31
+ type: "Article",
32
+ attributedTo: alice.href,
33
+ updated: sourceUpdated.toString(),
34
+ contentMap: {
35
+ en: "<p>Original text...</p>",
36
+ ko: "<p>번역 텍스트...</p>"
37
+ },
38
+ translations: [entry]
39
+ };
40
+ function translation() {
41
+ return new Translation({
42
+ language: new Intl.Locale("ko"),
43
+ translator: alice,
44
+ original: articleId,
45
+ sourceUpdated,
46
+ url: new URL(entry.url)
47
+ });
48
+ }
49
+ function article() {
50
+ return new Article({
51
+ id: articleId,
52
+ attribution: alice,
53
+ updated: sourceUpdated,
54
+ contents: [new LanguageString$1(example.contentMap.en, "en"), new LanguageString$1(example.contentMap.ko, "ko")],
55
+ translations: [translation()]
56
+ });
57
+ }
58
+ function record(value) {
59
+ ok(value != null && typeof value === "object" && !Array.isArray(value));
60
+ return value;
61
+ }
62
+ const noFetch = (url) => {
63
+ throw new Error(`Unexpected fetch: ${url}`);
64
+ };
65
+ function actorLoader(type, id) {
66
+ return (url) => Promise.resolve({
67
+ contextUrl: null,
68
+ documentUrl: url,
69
+ document: {
70
+ "@context": AS,
71
+ type,
72
+ id: id ?? url
73
+ }
74
+ });
75
+ }
76
+ test("FEP-22cd canonical context is preloaded", async () => {
77
+ ok(FEP in preloadedContexts);
78
+ deepStrictEqual(record((await getDocumentLoader()(FEP)).document)["@context"], {
79
+ "fep-22cd": `${FEP}#`,
80
+ schema: SCHEMA,
81
+ xsd: "http://www.w3.org/2001/XMLSchema#",
82
+ translations: {
83
+ "@id": "fep-22cd:translations",
84
+ "@container": "@set"
85
+ },
86
+ Translation: "fep-22cd:Translation",
87
+ sourceUpdated: {
88
+ "@id": "fep-22cd:sourceUpdated",
89
+ "@type": "xsd:dateTime"
90
+ },
91
+ translator: {
92
+ "@id": "schema:translator",
93
+ "@type": "@id",
94
+ "@container": "@set"
95
+ },
96
+ inLanguage: "schema:inLanguage",
97
+ translationOfWork: {
98
+ "@id": "schema:translationOfWork",
99
+ "@type": "@id"
100
+ },
101
+ isBasedOn: {
102
+ "@id": "schema:isBasedOn",
103
+ "@type": "@id"
104
+ }
105
+ });
106
+ });
107
+ test("FEP-22cd personal translation compact and expanded round trips", async () => {
108
+ for (const format of [
109
+ void 0,
110
+ "compact",
111
+ "expand"
112
+ ]) {
113
+ const encoded = await article().toJsonLd({ format });
114
+ if (format !== "expand") {
115
+ deepStrictEqual(record(encoded).translations, [entry]);
116
+ ok(record(encoded)["@context"].includes(FEP));
117
+ }
118
+ const restored = await Article.fromJsonLd(encoded, { documentLoader: noFetch });
119
+ deepStrictEqual(restored.attributionId, alice);
120
+ deepStrictEqual(restored.contents, article().contents);
121
+ deepStrictEqual(restored.translations.length, 1);
122
+ const t = restored.translations[0];
123
+ deepStrictEqual(t.language?.baseName, "ko");
124
+ deepStrictEqual(t.original, articleId);
125
+ deepStrictEqual(t.translatorIds, [alice]);
126
+ deepStrictEqual(t.sourceUpdated?.toString(), sourceUpdated.toString());
127
+ deepStrictEqual(t.url, new URL(entry.url));
128
+ deepStrictEqual(record(await restored.toJsonLd({ format: "compact" })).translations, [entry]);
129
+ }
130
+ deepStrictEqual(record(await (await Article.fromJsonLd(example, { documentLoader: noFetch })).toJsonLd({
131
+ format: "compact",
132
+ context
133
+ })).translations, [entry]);
134
+ deepStrictEqual(await article().toJsonLd({
135
+ format: "compact",
136
+ context
137
+ }), example);
138
+ });
139
+ test("FEP-22cd full IRIs, optional revision, script language and AS links", async () => {
140
+ const expanded = {
141
+ "@type": `${FEP}#Translation`,
142
+ [`${SCHEMA}inLanguage`]: "zh-Hant",
143
+ [`${SCHEMA}translator`]: [{ "@id": alice.href }],
144
+ [`${SCHEMA}translationOfWork`]: { "@id": articleId.href },
145
+ [`${SCHEMA}isBasedOn`]: { "@id": `${articleId.href}/revisions/2` },
146
+ [`${AS}#url`]: [{ "@id": `${articleId.href}/zh-Hant` }, {
147
+ "@type": `${AS}#Link`,
148
+ [`${AS}#href`]: { "@id": `${articleId.href}/zh-Hant.html` }
149
+ }]
150
+ };
151
+ const t = await Translation.fromJsonLd(expanded, { documentLoader: noFetch });
152
+ deepStrictEqual(t.language?.baseName, "zh-Hant");
153
+ deepStrictEqual(t.original, articleId);
154
+ deepStrictEqual(t.basis?.href, `${articleId.href}/revisions/2`);
155
+ deepStrictEqual(t.sourceUpdated, null);
156
+ ok(t.urls[1] instanceof Link);
157
+ const compact = record(await t.toJsonLd({ format: "compact" }));
158
+ deepStrictEqual(compact.translator, [alice.href]);
159
+ deepStrictEqual(compact.inLanguage, "zh-Hant");
160
+ deepStrictEqual(compact.isBasedOn, `${articleId.href}/revisions/2`);
161
+ ok(!("sourceUpdated" in compact));
162
+ ok(Object.keys(compact).every((key) => !key.startsWith("https:")));
163
+ });
164
+ test("FEP-22cd organization languages, multiple translators and review-only Update", async () => {
165
+ const initial = article().clone({
166
+ attribution: new URL("https://example.com/orgs/acme"),
167
+ names: [new LanguageString$1("Title", "en"), new LanguageString$1("제목", "ko")],
168
+ summaries: [new LanguageString$1("Summary", "en"), new LanguageString$1("요약", "ko")],
169
+ contents: [
170
+ ...article().contents,
171
+ new LanguageString$1("日本語版", "ja"),
172
+ "Fallback"
173
+ ],
174
+ translations: [translation(), new Translation({
175
+ language: new Intl.Locale("ja"),
176
+ translators: [new URL("https://example.com/users/bob"), new URL("https://example.com/actors/translator")],
177
+ original: articleId
178
+ })]
179
+ });
180
+ const editedAt = TemporalPolyfill.Instant.from("2026-09-15T00:00:00Z");
181
+ const edited = initial.clone({ updated: editedAt });
182
+ ok(TemporalPolyfill.Instant.compare(edited.translations[0].sourceUpdated, edited.updated) < 0);
183
+ const reviewed = edited.clone({ translations: [edited.translations[0].clone({ sourceUpdated: edited.updated }), edited.translations[1]] });
184
+ const update = new Update({
185
+ id: new URL(`${articleId.href}/review`),
186
+ actor: alice,
187
+ object: reviewed
188
+ });
189
+ const object = await (await Update.fromJsonLd(await update.toJsonLd({ format: "compact" }), { documentLoader: noFetch })).getObject();
190
+ ok(object instanceof Article);
191
+ deepStrictEqual(object.id, articleId);
192
+ const byLanguage = (values) => Object.fromEntries(values.map((value) => [value instanceof LanguageString$1 ? value.locale.baseName : "@none", value.toString()]));
193
+ deepStrictEqual(byLanguage(object.contents), byLanguage(initial.contents));
194
+ deepStrictEqual(object.names, initial.names);
195
+ deepStrictEqual(object.summaries, initial.summaries);
196
+ deepStrictEqual(object.attributionId, initial.attributionId);
197
+ deepStrictEqual(object.translations.map((t) => t.language?.baseName), ["ko", "ja"]);
198
+ deepStrictEqual(object.translations[0].sourceUpdated?.toString(), editedAt.toString());
199
+ deepStrictEqual(object.translations[1].sourceUpdated, null);
200
+ deepStrictEqual(object.translations[1].translatorIds, initial.translations[1].translatorIds);
201
+ const withdrawn = reviewed.clone({
202
+ translations: [],
203
+ contents: [new LanguageString$1("Original", "en")]
204
+ });
205
+ const replaced = await Article.fromJsonLd(await withdrawn.toJsonLd());
206
+ deepStrictEqual(replaced.translations, []);
207
+ deepStrictEqual(replaced.id, articleId);
208
+ });
209
+ test("FEP-22cd nested fresh and cached objects compact without full-IRI terms", async () => {
210
+ for (const child of [article(), await Article.fromJsonLd(example)]) for (const wrapper of [new Create({ object: child }), new Collection({ items: [child] })]) for (const format of [void 0, "compact"]) {
211
+ const json = record(await wrapper.toJsonLd({ format }));
212
+ deepStrictEqual(record(json.object ?? json.items).translations, [entry]);
213
+ const roundTrip = await Object$1.fromJsonLd(json);
214
+ ok(roundTrip instanceof Create || roundTrip instanceof Collection);
215
+ }
216
+ const custom = record(await article().toJsonLd({
217
+ format: "compact",
218
+ context: AS
219
+ }));
220
+ deepStrictEqual(custom["@context"], AS);
221
+ ok(`${FEP}#translations` in custom);
222
+ });
223
+ test("FEP-22cd all actor types use the existing linked-object getters", async () => {
224
+ for (const Type of [
225
+ Person,
226
+ Organization,
227
+ Group,
228
+ Application,
229
+ Service
230
+ ]) {
231
+ const t = new Translation({ translator: new Type({ id: alice }) });
232
+ ok(await t.getTranslator({ documentLoader: noFetch }) instanceof Type);
233
+ const parsed = await Translation.fromJsonLd(await t.toJsonLd());
234
+ ok(await parsed.getTranslator({ documentLoader: actorLoader(Type.name) }) instanceof Type);
235
+ deepStrictEqual((await Array.fromAsync(parsed.getTranslators())).length, 1);
236
+ }
237
+ });
238
+ test("FEP-22cd metadata ids are not fetched or trusted as actor origins", async () => {
239
+ for (const id of [void 0, "https://victim.example/metadata"]) for (const plural of [false, true]) {
240
+ const t = await Translation.fromJsonLd({
241
+ "@context": context,
242
+ type: "Translation",
243
+ ...id == null ? {} : { id },
244
+ translator: [{
245
+ id: "https://victim.example/actor",
246
+ type: "Application"
247
+ }]
248
+ });
249
+ const fetched = [];
250
+ const documentLoader = (url) => {
251
+ fetched.push(url);
252
+ return actorLoader("Person")(url);
253
+ };
254
+ const actors = plural ? await Array.fromAsync(t.getTranslators({ documentLoader })) : [await t.getTranslator({ documentLoader })];
255
+ deepStrictEqual(fetched, ["https://victim.example/actor"]);
256
+ ok(actors[0] instanceof Person);
257
+ deepStrictEqual(t.id?.href, id);
258
+ }
259
+ const parsed = await Article.fromJsonLd({
260
+ ...example,
261
+ translations: [{
262
+ ...entry,
263
+ id: "urn:translation:ko"
264
+ }]
265
+ }, { documentLoader: noFetch });
266
+ deepStrictEqual(parsed.translations[0].id?.href, "urn:translation:ko");
267
+ await parsed.toJsonLd({ format: "compact" });
268
+ });
269
+ async function hasCompleteCredit(t, documentLoader) {
270
+ const ids = t.translatorIds;
271
+ const actors = await Array.fromAsync(t.getTranslators({
272
+ documentLoader,
273
+ suppressError: true
274
+ }));
275
+ return actors.length > 0 && actors.length >= ids.length && actors.every((actor) => actor.id != null);
276
+ }
277
+ test("FEP-22cd unavailable, deleted, mismatched and id-less actors leave status unknown", async () => {
278
+ for (const failedLoader of [
279
+ noFetch,
280
+ actorLoader("Tombstone"),
281
+ actorLoader("Person", "https://spoof.example/actor")
282
+ ]) {
283
+ const t = new Translation({ translators: [alice, new Application({ id: new URL("https://example.com/machine") })] });
284
+ deepStrictEqual(await hasCompleteCredit(t, failedLoader), false);
285
+ deepStrictEqual(t.translatorIds, [alice, new URL("https://example.com/machine")]);
286
+ deepStrictEqual(record(await t.toJsonLd({ format: "compact" })).translator instanceof Array, true);
287
+ }
288
+ deepStrictEqual(await hasCompleteCredit(await Translation.fromJsonLd({
289
+ "@context": context,
290
+ type: "Translation",
291
+ translator: [alice.href, { type: "Application" }]
292
+ }), actorLoader("Person")), false);
293
+ await rejects(() => new Translation({ translator: alice }).getTranslator({ documentLoader: noFetch }), /Unexpected fetch/);
294
+ const t = new Translation({ translator: alice });
295
+ await rejects(() => t.getTranslator({
296
+ documentLoader: actorLoader("Person", "https://spoof.example/actor"),
297
+ crossOrigin: "throw"
298
+ }));
299
+ });
300
+ test("FEP-22cd malformed metadata follows vocabulary parsing behavior", async () => {
301
+ await rejects(() => Article.fromJsonLd({
302
+ ...example,
303
+ translations: [{
304
+ ...entry,
305
+ inLanguage: "not_a_language"
306
+ }]
307
+ }), RangeError);
308
+ await rejects(() => Article.fromJsonLd({
309
+ ...example,
310
+ translations: [{
311
+ ...entry,
312
+ sourceUpdated: "2026-09-01"
313
+ }]
314
+ }), RangeError);
315
+ await rejects(() => Article.fromJsonLd({
316
+ ...example,
317
+ translations: [{
318
+ ...entry,
319
+ type: "Note"
320
+ }]
321
+ }), TypeError);
322
+ const idOnly = await Article.fromJsonLd({
323
+ ...example,
324
+ translations: [{ id: "urn:translation:ko" }]
325
+ }, { documentLoader: noFetch });
326
+ deepStrictEqual(idOnly.translations[0].id?.href, "urn:translation:ko");
327
+ deepStrictEqual(idOnly.translations[0].language, null);
328
+ deepStrictEqual(idOnly.translations[0].translatorIds, []);
329
+ deepStrictEqual((await Article.fromJsonLd({
330
+ ...example,
331
+ translations: void 0
332
+ })).translations, []);
333
+ });
334
+ test("FEP-22cd ordinary multilingual serialization remains unchanged", async () => {
335
+ const fields = {
336
+ id: articleId,
337
+ attribution: alice,
338
+ contents: [
339
+ "Fallback",
340
+ new LanguageString$1("Original", "en"),
341
+ new LanguageString$1("번역", "ko")
342
+ ],
343
+ names: [new LanguageString$1("Title", "en"), new LanguageString$1("제목", "ko")]
344
+ };
345
+ for (const Type of [
346
+ Article,
347
+ Note,
348
+ Object$1
349
+ ]) for (const format of [void 0, "compact"]) {
350
+ const json = record(await new Type(fields).toJsonLd({ format }));
351
+ ok(!JSON.stringify(json["@context"]).includes(FEP));
352
+ deepStrictEqual(json.contentMap, {
353
+ en: "Original",
354
+ ko: "번역"
355
+ });
356
+ deepStrictEqual(json.content, "Fallback");
357
+ deepStrictEqual(json.nameMap, {
358
+ en: "Title",
359
+ ko: "제목"
360
+ });
361
+ deepStrictEqual(json.attributedTo, alice.href);
362
+ ok(!("translations" in json));
363
+ }
364
+ });
365
+ test("FEP-22cd keeps Mastodon PropertyValue and Schema.org translation namespaces distinct", async () => {
366
+ const json = record(await new Person({
367
+ id: alice,
368
+ attachments: [new PropertyValue({
369
+ name: "Website",
370
+ value: "https://example.com/"
371
+ })],
372
+ translations: [translation()]
373
+ }).toJsonLd());
374
+ deepStrictEqual(json.attachment, {
375
+ type: "PropertyValue",
376
+ name: "Website",
377
+ value: "https://example.com/"
378
+ });
379
+ deepStrictEqual(json.translations, [entry]);
380
+ const restored = await Person.fromJsonLd(json);
381
+ const attachments = await Array.fromAsync(restored.getAttachments());
382
+ ok(attachments[0] instanceof PropertyValue);
383
+ deepStrictEqual(attachments[0].value, "https://example.com/");
384
+ deepStrictEqual(restored.translations[0].original, articleId);
385
+ });
386
+ test("FEP-22cd explicit actor trust remains opt-in and clones retain untrusted state", async () => {
387
+ const parsed = await Translation.fromJsonLd({
388
+ "@context": context,
389
+ type: "Translation",
390
+ id: "https://victim.example/metadata",
391
+ translator: [{
392
+ id: "https://victim.example/actor",
393
+ type: "Application"
394
+ }]
395
+ });
396
+ ok(await parsed.getTranslator({
397
+ documentLoader: noFetch,
398
+ crossOrigin: "trust"
399
+ }) instanceof Application);
400
+ ok(await parsed.clone({ sourceUpdated }).getTranslator({ documentLoader: actorLoader("Person") }) instanceof Person);
401
+ ok(await parsed.clone({ translator: new Application({ id: alice }) }).getTranslator({ documentLoader: noFetch }) instanceof Application);
402
+ });
403
+ //#endregion
404
+ export {};
@@ -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"
@@ -1,6 +1,6 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { M as Person, U as getEntityTypeById, W as isEntityType, w as Link } from "./vocab-CNiWKo57.mjs";
3
+ import { K as getEntityTypeById, N as Person, T as Link, q as isEntityType } from "./vocab-D3s0_CFf.mjs";
4
4
  import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
5
5
  import { test } from "@fedify/fixture";
6
6
  import { deepStrictEqual } from "node:assert/strict";