@fedify/vocab 2.4.0-pr.936.41 → 2.4.0

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.
Files changed (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +31328 -8723
  3. package/dist/mod.d.cts +4599 -6
  4. package/dist/mod.d.ts +4599 -6
  5. package/dist/mod.js +31221 -8623
  6. package/dist-tests/actor-D7_iX4I4.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-COZcxf00.mjs} +30831 -8515
  19. package/dist-tests/vocab.test.mjs +677 -10
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +914 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /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"