@_linked/translation 0.2.5 → 0.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.
- package/lib/esm/archive.d.ts +12 -3
- package/lib/esm/archive.js +23 -9
- package/lib/esm/archive.js.map +1 -1
- package/lib/esm/formats/csv.d.ts +1 -1
- package/lib/esm/formats/csv.js +11 -6
- package/lib/esm/formats/csv.js.map +1 -1
- package/lib/esm/formats/json.js +19 -14
- package/lib/esm/formats/json.js.map +1 -1
- package/lib/esm/formats/xliff.js +47 -31
- package/lib/esm/formats/xliff.js.map +1 -1
- package/lib/esm/shapes/TranslationProvider.d.ts +1 -1
- package/lib/esm/shapes/TranslationProvider.js +1 -1
- package/package.json +2 -2
- package/src/archive.ts +31 -11
- package/src/formats/csv.ts +14 -6
- package/src/formats/json.ts +28 -19
- package/src/formats/xliff.ts +54 -30
- package/src/shapes/TranslationProvider.ts +1 -1
package/src/formats/xliff.ts
CHANGED
|
@@ -10,7 +10,27 @@ import {
|
|
|
10
10
|
import type { TranslationState } from '../records.js';
|
|
11
11
|
|
|
12
12
|
const UTF8 = new TextEncoder();
|
|
13
|
-
|
|
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
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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
|
-
[
|
|
549
|
-
[
|
|
550
|
-
[
|
|
551
|
-
[
|
|
552
|
-
[
|
|
553
|
-
[
|
|
554
|
-
[
|
|
555
|
-
[
|
|
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
|
-
[
|
|
565
|
-
[
|
|
566
|
-
[
|
|
567
|
-
[
|
|
568
|
-
[
|
|
569
|
-
[
|
|
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="
|
|
638
|
+
? `<note from="${NOTE_ROLES.description[0]}">${escapeText(entry.description)}</note>`
|
|
615
639
|
: '',
|
|
616
640
|
target?.note
|
|
617
|
-
? `<note from="
|
|
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)}"
|
|
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="
|
|
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
|
|
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="
|
|
670
|
+
? `<note category="${NOTE_ROLES.description[0]}">${escapeText(entry.description)}</note>`
|
|
647
671
|
: ''
|
|
648
672
|
}${
|
|
649
673
|
target?.note
|
|
650
|
-
? `<note category="
|
|
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)}"
|
|
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 ? `
|
|
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
|
|
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
|
|
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
|