@_linked/translation 0.2.5 → 0.3.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.
@@ -10,7 +10,27 @@ import {
10
10
  import type { TranslationState } from '../records.js';
11
11
 
12
12
  const UTF8 = new TextEncoder();
13
- const CN_NAMESPACE = 'https://create-now.app/ns/translation-exchange/1';
13
+ /**
14
+ * Our own metadata namespace, a sibling of the translation vocabulary
15
+ * (`https://id.linked.cm/translation/vocab#`). Files exported before 0.3.0
16
+ * used a Create Now URI and a `cn:` prefix; the reader below still accepts
17
+ * both, so those files keep importing.
18
+ */
19
+ const EXCHANGE_NAMESPACE = 'https://id.linked.cm/translation/exchange/1';
20
+ const PREFIX = 'lt';
21
+ const LEGACY_PREFIX = 'cn';
22
+ /** `from`/`category` on a `<note>`, in current and pre-0.3.0 spelling. */
23
+ const NOTE_ROLES = {
24
+ description: ['linked-description', 'create-now-description'],
25
+ target: ['linked-target', 'create-now-target'],
26
+ } as const;
27
+
28
+ function isNoteRole(
29
+ value: string | undefined,
30
+ role: keyof typeof NOTE_ROLES,
31
+ ): boolean {
32
+ return value !== undefined && (NOTE_ROLES[role] as readonly string[]).includes(value);
33
+ }
14
34
  const XLIFF_12_NAMESPACE = 'urn:oasis:names:tc:xliff:document:1.2';
15
35
  const XLIFF_20_NAMESPACE = 'urn:oasis:names:tc:xliff:document:2.0';
16
36
  const XML_MEDIA_TYPE = 'application/xliff+xml';
@@ -301,7 +321,11 @@ function plainText(node: XmlNode | undefined): string | undefined {
301
321
  }
302
322
 
303
323
  function metadata(node: XmlNode, name: string): string | undefined {
304
- return node.attributes[`cn:${name}`] ?? attribute(node, name);
324
+ return (
325
+ node.attributes[`${PREFIX}:${name}`] ??
326
+ node.attributes[`${LEGACY_PREFIX}:${name}`] ??
327
+ attribute(node, name)
328
+ );
305
329
  }
306
330
 
307
331
  function recognizedState(value: string | undefined): TranslationState | undefined {
@@ -419,12 +443,12 @@ function parse12(
419
443
  if (language) targetLanguages.add(language);
420
444
  const notes = children(unit, 'note');
421
445
  const description = plainText(
422
- notes.find((note) => attribute(note, 'from') === 'create-now-description'),
446
+ notes.find((note) => isNoteRole(attribute(note, 'from'), 'description')),
423
447
  );
424
448
  const targetNote = plainText(
425
449
  notes.find(
426
450
  (note) =>
427
- attribute(note, 'from') === 'create-now-target' &&
451
+ isNoteRole(attribute(note, 'from'), 'target') &&
428
452
  (!language || !attribute(note, 'lang') || attribute(note, 'lang') === language),
429
453
  ),
430
454
  );
@@ -475,13 +499,13 @@ function parse20(
475
499
  const notes = notesContainer ? children(notesContainer, 'note') : [];
476
500
  const description = plainText(
477
501
  notes.find(
478
- (note) => attribute(note, 'category') === 'create-now-description',
502
+ (note) => isNoteRole(attribute(note, 'category'), 'description'),
479
503
  ),
480
504
  );
481
505
  const targetNote = plainText(
482
506
  notes.find(
483
507
  (note) =>
484
- attribute(note, 'category') === 'create-now-target' &&
508
+ isNoteRole(attribute(note, 'category'), 'target') &&
485
509
  (!language || !attribute(note, 'lang') || attribute(note, 'lang') === language),
486
510
  ),
487
511
  );
@@ -545,14 +569,14 @@ function state20(state: TranslationState | undefined): string {
545
569
 
546
570
  function metadataAttributes(entry: TranslationExchangeEntry): string {
547
571
  return [
548
- ['cn:key', entry.key],
549
- ['cn:namespace', entry.namespace],
550
- ['cn:kind', entry.kind],
551
- ['cn:format', entry.format],
552
- ['cn:key-version-id', entry.keyVersionId],
553
- ['cn:source-hash', entry.sourceHash],
554
- ['cn:contract-hash', entry.contractHash],
555
- ['cn:argument-signature', entry.argumentSignature],
572
+ [`${PREFIX}:key`, entry.key],
573
+ [`${PREFIX}:namespace`, entry.namespace],
574
+ [`${PREFIX}:kind`, entry.kind],
575
+ [`${PREFIX}:format`, entry.format],
576
+ [`${PREFIX}:key-version-id`, entry.keyVersionId],
577
+ [`${PREFIX}:source-hash`, entry.sourceHash],
578
+ [`${PREFIX}:contract-hash`, entry.contractHash],
579
+ [`${PREFIX}:argument-signature`, entry.argumentSignature],
556
580
  ]
557
581
  .filter((pair): pair is [string, string] => pair[1] !== undefined)
558
582
  .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
@@ -561,12 +585,12 @@ function metadataAttributes(entry: TranslationExchangeEntry): string {
561
585
 
562
586
  function rootMetadata(document: TranslationExchangeDocument): string {
563
587
  return [
564
- ['cn:schema-version', String(document.schemaVersion)],
565
- ['cn:app-id', document.appId],
566
- ['cn:branch-id', document.branchId],
567
- ['cn:exported-at', document.exportedAt],
568
- ['cn:revision-watermark', document.revisionWatermark],
569
- ['cn:contract-set-hash', document.contractSetHash],
588
+ [`${PREFIX}:schema-version`, String(document.schemaVersion)],
589
+ [`${PREFIX}:app-id`, document.appId],
590
+ [`${PREFIX}:branch-id`, document.branchId],
591
+ [`${PREFIX}:exported-at`, document.exportedAt],
592
+ [`${PREFIX}:revision-watermark`, document.revisionWatermark],
593
+ [`${PREFIX}:contract-set-hash`, document.contractSetHash],
570
594
  ]
571
595
  .filter((pair): pair is [string, string] => pair[1] !== undefined)
572
596
  .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
@@ -611,25 +635,25 @@ function serialize12(document: TranslationExchangeDocument): string {
611
635
  const id = deterministicId(entry, index);
612
636
  const notes = [
613
637
  entry.description
614
- ? `<note from="create-now-description">${escapeText(entry.description)}</note>`
638
+ ? `<note from="${NOTE_ROLES.description[0]}">${escapeText(entry.description)}</note>`
615
639
  : '',
616
640
  target?.note
617
- ? `<note from="create-now-target" xml:lang="${escapeAttribute(language!)}">${escapeText(target.note)}</note>`
641
+ ? `<note from="${NOTE_ROLES.target[0]}" xml:lang="${escapeAttribute(language!)}">${escapeText(target.note)}</note>`
618
642
  : '',
619
643
  ].join('');
620
644
  return `<trans-unit id="${escapeAttribute(id)}" resname="${escapeAttribute(entry.key)}"${metadataAttributes(entry)}><source>${validatedMixedContent(entry.sourceText, INLINE_12)}</source>${
621
645
  language && target
622
- ? `<target xml:lang="${escapeAttribute(language)}" state="${state12(target?.state)}" cn:state="${escapeAttribute(target?.state ?? 'untranslated')}">${validatedMixedContent(target?.text ?? '', INLINE_12)}</target>`
646
+ ? `<target xml:lang="${escapeAttribute(language)}" state="${state12(target?.state)}" ${PREFIX}:state="${escapeAttribute(target?.state ?? 'untranslated')}">${validatedMixedContent(target?.text ?? '', INLINE_12)}</target>`
623
647
  : ''
624
648
  }${notes}</trans-unit>`;
625
649
  })
626
650
  .join('');
627
- return `<file id="f${fileIndex + 1}" original="create-now" datatype="plaintext" source-language="${escapeAttribute(document.sourceLanguage)}"${
651
+ return `<file id="f${fileIndex + 1}" original="linked" datatype="plaintext" source-language="${escapeAttribute(document.sourceLanguage)}"${
628
652
  language ? ` target-language="${escapeAttribute(language)}"` : ''
629
653
  }><body>${units}</body></file>`;
630
654
  })
631
655
  .join('');
632
- return `<?xml version="1.0" encoding="UTF-8"?>\n<xliff xmlns="${XLIFF_12_NAMESPACE}" xmlns:cn="${CN_NAMESPACE}" version="1.2"${rootMetadata(document)}>${files}</xliff>\n`;
656
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<xliff xmlns="${XLIFF_12_NAMESPACE}" xmlns:${PREFIX}="${EXCHANGE_NAMESPACE}" version="1.2"${rootMetadata(document)}>${files}</xliff>\n`;
633
657
  }
634
658
 
635
659
  function serialize20(document: TranslationExchangeDocument): string {
@@ -643,15 +667,15 @@ function serialize20(document: TranslationExchangeDocument): string {
643
667
  entry.description || target?.note
644
668
  ? `<notes>${
645
669
  entry.description
646
- ? `<note category="create-now-description">${escapeText(entry.description)}</note>`
670
+ ? `<note category="${NOTE_ROLES.description[0]}">${escapeText(entry.description)}</note>`
647
671
  : ''
648
672
  }${
649
673
  target?.note
650
- ? `<note category="create-now-target" xml:lang="${escapeAttribute(language!)}">${escapeText(target.note)}</note>`
674
+ ? `<note category="${NOTE_ROLES.target[0]}" xml:lang="${escapeAttribute(language!)}">${escapeText(target.note)}</note>`
651
675
  : ''
652
676
  }</notes>`
653
677
  : '';
654
- return `<unit id="${escapeAttribute(id)}" name="${escapeAttribute(entry.key)}"${metadataAttributes(entry)}>${notes}<segment id="s1" state="${state20(target?.state)}" cn:state="${escapeAttribute(target?.state ?? 'untranslated')}"><source>${validatedMixedContent(entry.sourceText, INLINE_20)}</source>${
678
+ return `<unit id="${escapeAttribute(id)}" name="${escapeAttribute(entry.key)}"${metadataAttributes(entry)}>${notes}<segment id="s1" state="${state20(target?.state)}" ${PREFIX}:state="${escapeAttribute(target?.state ?? 'untranslated')}"><source>${validatedMixedContent(entry.sourceText, INLINE_20)}</source>${
655
679
  language && target
656
680
  ? `<target xml:lang="${escapeAttribute(language)}">${validatedMixedContent(target?.text ?? '', INLINE_20)}</target>`
657
681
  : ''
@@ -659,7 +683,7 @@ function serialize20(document: TranslationExchangeDocument): string {
659
683
  })
660
684
  .join('');
661
685
  return `<file id="f${fileIndex + 1}"${
662
- language ? ` cn:target-language="${escapeAttribute(language)}"` : ''
686
+ language ? ` ${PREFIX}:target-language="${escapeAttribute(language)}"` : ''
663
687
  }>${units}</file>`;
664
688
  })
665
689
  .join('');
@@ -667,7 +691,7 @@ function serialize20(document: TranslationExchangeDocument): string {
667
691
  document.targetLanguages.length === 1
668
692
  ? ` trgLang="${escapeAttribute(document.targetLanguages[0])}"`
669
693
  : '';
670
- return `<?xml version="1.0" encoding="UTF-8"?>\n<xliff xmlns="${XLIFF_20_NAMESPACE}" xmlns:cn="${CN_NAMESPACE}" version="2.0" srcLang="${escapeAttribute(document.sourceLanguage)}"${singleTarget}${rootMetadata(document)}>${files}</xliff>\n`;
694
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<xliff xmlns="${XLIFF_20_NAMESPACE}" xmlns:${PREFIX}="${EXCHANGE_NAMESPACE}" version="2.0" srcLang="${escapeAttribute(document.sourceLanguage)}"${singleTarget}${rootMetadata(document)}>${files}</xliff>\n`;
671
695
  }
672
696
 
673
697
  function sniffVersion(
@@ -1426,7 +1426,7 @@ export class TranslationProvider extends ShapeProvider {
1426
1426
 
1427
1427
  /**
1428
1428
  * Authoring gate. Reads `linkedAuth` off the request the LINKED server already
1429
- * populates — framework-level, so this stays portable (no CN/create-now-js
1429
+ * populates — framework-level, so this stays portable (no Create Now
1430
1430
  * import, AD-G). Anonymous callers can no longer read or write an app's
1431
1431
  * translation content via `/call`. Fine-grained "is translation enabled for
1432
1432
  * this app + is the caller a project member" gating is a Create Now concern and