@_linked/translation 0.2.2
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/README.md +102 -0
- package/lib/cjs/data/translation.json +8 -0
- package/lib/esm/archive.d.ts +133 -0
- package/lib/esm/archive.js +567 -0
- package/lib/esm/archive.js.map +1 -0
- package/lib/esm/authorization.d.ts +12 -0
- package/lib/esm/authorization.js +10 -0
- package/lib/esm/authorization.js.map +1 -0
- package/lib/esm/backend.d.ts +1 -0
- package/lib/esm/backend.js +3 -0
- package/lib/esm/backend.js.map +1 -0
- package/lib/esm/cdn-loader.d.ts +40 -0
- package/lib/esm/cdn-loader.js +200 -0
- package/lib/esm/cdn-loader.js.map +1 -0
- package/lib/esm/checks.d.ts +56 -0
- package/lib/esm/checks.js +288 -0
- package/lib/esm/checks.js.map +1 -0
- package/lib/esm/compile.d.ts +64 -0
- package/lib/esm/compile.js +99 -0
- package/lib/esm/compile.js.map +1 -0
- package/lib/esm/core/messages.d.ts +25 -0
- package/lib/esm/core/messages.js +76 -0
- package/lib/esm/core/messages.js.map +1 -0
- package/lib/esm/data/translation.json +8 -0
- package/lib/esm/discovery.d.ts +97 -0
- package/lib/esm/discovery.js +365 -0
- package/lib/esm/discovery.js.map +1 -0
- package/lib/esm/exchange.d.ts +100 -0
- package/lib/esm/exchange.js +223 -0
- package/lib/esm/exchange.js.map +1 -0
- package/lib/esm/export.d.ts +28 -0
- package/lib/esm/export.js +92 -0
- package/lib/esm/export.js.map +1 -0
- package/lib/esm/formats/csv.d.ts +8 -0
- package/lib/esm/formats/csv.js +397 -0
- package/lib/esm/formats/csv.js.map +1 -0
- package/lib/esm/formats/json.d.ts +30 -0
- package/lib/esm/formats/json.js +459 -0
- package/lib/esm/formats/json.js.map +1 -0
- package/lib/esm/formats/xliff.d.ts +12 -0
- package/lib/esm/formats/xliff.js +577 -0
- package/lib/esm/formats/xliff.js.map +1 -0
- package/lib/esm/import.d.ts +197 -0
- package/lib/esm/import.js +391 -0
- package/lib/esm/import.js.map +1 -0
- package/lib/esm/index.d.ts +42 -0
- package/lib/esm/index.js +34 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/key-sync-node.d.ts +29 -0
- package/lib/esm/key-sync-node.js +174 -0
- package/lib/esm/key-sync-node.js.map +1 -0
- package/lib/esm/key-sync.d.ts +46 -0
- package/lib/esm/key-sync.js +114 -0
- package/lib/esm/key-sync.js.map +1 -0
- package/lib/esm/key-version.d.ts +50 -0
- package/lib/esm/key-version.js +134 -0
- package/lib/esm/key-version.js.map +1 -0
- package/lib/esm/languages.d.ts +18 -0
- package/lib/esm/languages.js +74 -0
- package/lib/esm/languages.js.map +1 -0
- package/lib/esm/linked-discovery.d.ts +44 -0
- package/lib/esm/linked-discovery.js +267 -0
- package/lib/esm/linked-discovery.js.map +1 -0
- package/lib/esm/message-format.d.ts +41 -0
- package/lib/esm/message-format.js +133 -0
- package/lib/esm/message-format.js.map +1 -0
- package/lib/esm/mt.d.ts +111 -0
- package/lib/esm/mt.js +142 -0
- package/lib/esm/mt.js.map +1 -0
- package/lib/esm/ontologies/translation.d.ts +203 -0
- package/lib/esm/ontologies/translation.js +219 -0
- package/lib/esm/ontologies/translation.js.map +1 -0
- package/lib/esm/package.d.ts +4 -0
- package/lib/esm/package.js +11 -0
- package/lib/esm/package.js.map +1 -0
- package/lib/esm/react.d.ts +55 -0
- package/lib/esm/react.js +127 -0
- package/lib/esm/react.js.map +1 -0
- package/lib/esm/records.d.ts +151 -0
- package/lib/esm/records.js +2 -0
- package/lib/esm/records.js.map +1 -0
- package/lib/esm/release.d.ts +198 -0
- package/lib/esm/release.js +435 -0
- package/lib/esm/release.js.map +1 -0
- package/lib/esm/shape-catalog.d.ts +79 -0
- package/lib/esm/shape-catalog.js +118 -0
- package/lib/esm/shape-catalog.js.map +1 -0
- package/lib/esm/shapes/GlossaryTerm.d.ts +28 -0
- package/lib/esm/shapes/GlossaryTerm.js +94 -0
- package/lib/esm/shapes/GlossaryTerm.js.map +1 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.d.ts +33 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.js +209 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.js.map +1 -0
- package/lib/esm/shapes/TranslationKey.d.ts +92 -0
- package/lib/esm/shapes/TranslationKey.js +194 -0
- package/lib/esm/shapes/TranslationKey.js.map +1 -0
- package/lib/esm/shapes/TranslationKeyVersion.d.ts +19 -0
- package/lib/esm/shapes/TranslationKeyVersion.js +125 -0
- package/lib/esm/shapes/TranslationKeyVersion.js.map +1 -0
- package/lib/esm/shapes/TranslationLanguage.d.ts +15 -0
- package/lib/esm/shapes/TranslationLanguage.js +102 -0
- package/lib/esm/shapes/TranslationLanguage.js.map +1 -0
- package/lib/esm/shapes/TranslationProvider.d.ts +409 -0
- package/lib/esm/shapes/TranslationProvider.js +1376 -0
- package/lib/esm/shapes/TranslationProvider.js.map +1 -0
- package/lib/esm/shapes/TranslationRelease.d.ts +36 -0
- package/lib/esm/shapes/TranslationRelease.js +149 -0
- package/lib/esm/shapes/TranslationRelease.js.map +1 -0
- package/lib/esm/shapes/TranslationRevision.d.ts +39 -0
- package/lib/esm/shapes/TranslationRevision.js +159 -0
- package/lib/esm/shapes/TranslationRevision.js.map +1 -0
- package/lib/esm/shapes/TranslationUnit.d.ts +26 -0
- package/lib/esm/shapes/TranslationUnit.js +111 -0
- package/lib/esm/shapes/TranslationUnit.js.map +1 -0
- package/package.json +134 -0
- package/src/archive.ts +894 -0
- package/src/authorization.ts +33 -0
- package/src/backend.ts +2 -0
- package/src/cdn-loader.ts +264 -0
- package/src/checks.ts +413 -0
- package/src/compile.ts +168 -0
- package/src/core/messages.ts +106 -0
- package/src/data/translation.json +8 -0
- package/src/discovery.ts +594 -0
- package/src/exchange.ts +428 -0
- package/src/export.ts +135 -0
- package/src/formats/csv.ts +459 -0
- package/src/formats/json.ts +587 -0
- package/src/formats/xliff.ts +708 -0
- package/src/import.ts +648 -0
- package/src/index.ts +207 -0
- package/src/key-sync-node.ts +269 -0
- package/src/key-sync.ts +168 -0
- package/src/key-version.ts +206 -0
- package/src/languages.ts +102 -0
- package/src/linked-discovery.ts +385 -0
- package/src/message-format.ts +214 -0
- package/src/mt.ts +248 -0
- package/src/ontologies/translation.ts +234 -0
- package/src/package.ts +19 -0
- package/src/react.tsx +250 -0
- package/src/records.ts +180 -0
- package/src/release.ts +726 -0
- package/src/shape-catalog.ts +198 -0
- package/src/shapes/GlossaryTerm.ts +61 -0
- package/src/shapes/TranslationInventoryOrigin.ts +131 -0
- package/src/shapes/TranslationKey.ts +174 -0
- package/src/shapes/TranslationKeyVersion.ts +77 -0
- package/src/shapes/TranslationLanguage.ts +51 -0
- package/src/shapes/TranslationProvider.ts +1755 -0
- package/src/shapes/TranslationRelease.ts +114 -0
- package/src/shapes/TranslationRevision.ts +105 -0
- package/src/shapes/TranslationUnit.ts +72 -0
package/src/exchange.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import type { MessageFormat } from './core/messages.js';
|
|
2
|
+
import { canonicalLanguageTag } from './key-version.js';
|
|
3
|
+
import type { TranslationState } from './records.js';
|
|
4
|
+
|
|
5
|
+
export type TranslationExchangeKind = 'ui' | 'semantic';
|
|
6
|
+
export type TranslationDeclarationKind =
|
|
7
|
+
| TranslationExchangeKind
|
|
8
|
+
| 'content';
|
|
9
|
+
export type TranslationDeclarationStatus = 'confirmed' | 'pending';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The build-facing subset of the later multi-source discovery contract.
|
|
13
|
+
* Static extraction omits the optional fields and retains its existing
|
|
14
|
+
* confirmed/simple behavior.
|
|
15
|
+
*/
|
|
16
|
+
export interface TranslationBuildDeclaration {
|
|
17
|
+
key: string;
|
|
18
|
+
sourceText: string;
|
|
19
|
+
format?: MessageFormat;
|
|
20
|
+
kind?: TranslationDeclarationKind;
|
|
21
|
+
status?: TranslationDeclarationStatus;
|
|
22
|
+
authoritative?: boolean;
|
|
23
|
+
namespace?: string;
|
|
24
|
+
file?: string;
|
|
25
|
+
line?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TranslationExchangeValue {
|
|
29
|
+
text: string;
|
|
30
|
+
state?: TranslationState;
|
|
31
|
+
note?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TranslationExchangeEntry {
|
|
35
|
+
key: string;
|
|
36
|
+
namespace?: string;
|
|
37
|
+
kind: TranslationExchangeKind;
|
|
38
|
+
sourceText: string;
|
|
39
|
+
keyVersionId: string;
|
|
40
|
+
sourceHash: string;
|
|
41
|
+
contractHash: string;
|
|
42
|
+
argumentSignature: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
format: MessageFormat;
|
|
45
|
+
translations: Record<string, TranslationExchangeValue>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TranslationExchangeDocument {
|
|
49
|
+
schemaVersion: 1;
|
|
50
|
+
appId?: string;
|
|
51
|
+
branchId?: string;
|
|
52
|
+
sourceLanguage: string;
|
|
53
|
+
targetLanguages: string[];
|
|
54
|
+
exportedAt?: string;
|
|
55
|
+
revisionWatermark?: string;
|
|
56
|
+
contractSetHash?: string;
|
|
57
|
+
entries: TranslationExchangeEntry[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ParsedTranslationExchangeEntry = Omit<
|
|
61
|
+
TranslationExchangeEntry,
|
|
62
|
+
| 'sourceText'
|
|
63
|
+
| 'keyVersionId'
|
|
64
|
+
| 'sourceHash'
|
|
65
|
+
| 'contractHash'
|
|
66
|
+
| 'argumentSignature'
|
|
67
|
+
> & {
|
|
68
|
+
sourceText?: string;
|
|
69
|
+
keyVersionId?: string;
|
|
70
|
+
sourceHash?: string;
|
|
71
|
+
contractHash?: string;
|
|
72
|
+
argumentSignature?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export interface ParsedTranslationExchangeDocument
|
|
76
|
+
extends Omit<TranslationExchangeDocument, 'entries'> {
|
|
77
|
+
entries: ParsedTranslationExchangeEntry[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TranslationFormat =
|
|
81
|
+
| 'xliff-1.2'
|
|
82
|
+
| 'xliff-2.0'
|
|
83
|
+
| 'csv'
|
|
84
|
+
| 'json';
|
|
85
|
+
|
|
86
|
+
export interface SerializedTranslationDocument {
|
|
87
|
+
fileName: string;
|
|
88
|
+
mediaType: string;
|
|
89
|
+
body: Uint8Array;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TranslationFormatAdapter {
|
|
93
|
+
readonly format: TranslationFormat;
|
|
94
|
+
sniff(input: Uint8Array, fileName?: string): boolean;
|
|
95
|
+
parse(
|
|
96
|
+
input: Uint8Array,
|
|
97
|
+
options?: unknown,
|
|
98
|
+
): Promise<ParsedTranslationExchangeDocument>;
|
|
99
|
+
serialize(
|
|
100
|
+
document: TranslationExchangeDocument,
|
|
101
|
+
options?: unknown,
|
|
102
|
+
): Promise<SerializedTranslationDocument>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type TranslationImportDisposition =
|
|
106
|
+
| 'import'
|
|
107
|
+
| 'carry-forward'
|
|
108
|
+
| 'unverified-source'
|
|
109
|
+
| 'conflict'
|
|
110
|
+
| 'skip';
|
|
111
|
+
|
|
112
|
+
export interface TranslationImportDryRunEntry {
|
|
113
|
+
key: string;
|
|
114
|
+
language: string;
|
|
115
|
+
disposition: TranslationImportDisposition;
|
|
116
|
+
expectedKeyVersionId?: string;
|
|
117
|
+
expectedUnitContentHash?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface TranslationImportDryRun {
|
|
121
|
+
sessionId: string;
|
|
122
|
+
documentHash: string;
|
|
123
|
+
revisionWatermark: string;
|
|
124
|
+
expiresAt: string;
|
|
125
|
+
entries: TranslationImportDryRunEntry[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const EXCHANGE_STATES = new Set<TranslationState>([
|
|
129
|
+
'untranslated',
|
|
130
|
+
'machine',
|
|
131
|
+
'reviewed',
|
|
132
|
+
'stale',
|
|
133
|
+
]);
|
|
134
|
+
const adapters = new Map<TranslationFormat, TranslationFormatAdapter>();
|
|
135
|
+
|
|
136
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
137
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
throw new Error(`${label} must be an object.`);
|
|
139
|
+
}
|
|
140
|
+
return value as Record<string, unknown>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function requiredString(value: unknown, label: string): string {
|
|
144
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
145
|
+
throw new Error(`${label} is required.`);
|
|
146
|
+
}
|
|
147
|
+
return value.trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function optionalString(value: unknown, label: string): string | undefined {
|
|
151
|
+
if (value === undefined) return undefined;
|
|
152
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
153
|
+
throw new Error(`${label} must be a non-empty string when supplied.`);
|
|
154
|
+
}
|
|
155
|
+
return value.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function messageFormat(value: unknown, label: string): MessageFormat {
|
|
159
|
+
if (value !== 'simple' && value !== 'icu') {
|
|
160
|
+
throw new Error(`${label} must be "simple" or "icu".`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function exchangeKind(value: unknown, label: string): TranslationExchangeKind {
|
|
166
|
+
if (value !== 'ui' && value !== 'semantic') {
|
|
167
|
+
throw new Error(`${label} must be "ui" or "semantic".`);
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function canonicalLanguages(value: unknown, label: string): string[] {
|
|
173
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
174
|
+
const canonical: string[] = [];
|
|
175
|
+
const seen = new Set<string>();
|
|
176
|
+
for (const item of value) {
|
|
177
|
+
if (typeof item !== 'string') {
|
|
178
|
+
throw new Error(`${label} must contain only BCP-47 strings.`);
|
|
179
|
+
}
|
|
180
|
+
const language = canonicalLanguageTag(item);
|
|
181
|
+
if (!seen.has(language)) {
|
|
182
|
+
seen.add(language);
|
|
183
|
+
canonical.push(language);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return canonical;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeTranslations(
|
|
190
|
+
value: unknown,
|
|
191
|
+
label: string,
|
|
192
|
+
): Record<string, TranslationExchangeValue> {
|
|
193
|
+
const input = record(value, label);
|
|
194
|
+
const translations: Record<string, TranslationExchangeValue> = {};
|
|
195
|
+
for (const [rawLanguage, rawValue] of Object.entries(input)) {
|
|
196
|
+
const language = canonicalLanguageTag(rawLanguage);
|
|
197
|
+
if (translations[language]) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${label} contains duplicate canonical language "${language}".`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const item = record(rawValue, `${label}.${rawLanguage}`);
|
|
203
|
+
if (typeof item.text !== 'string') {
|
|
204
|
+
throw new Error(`${label}.${rawLanguage}.text must be a string.`);
|
|
205
|
+
}
|
|
206
|
+
const state = item.state;
|
|
207
|
+
if (state !== undefined && !EXCHANGE_STATES.has(state as TranslationState)) {
|
|
208
|
+
throw new Error(`${label}.${rawLanguage}.state is invalid.`);
|
|
209
|
+
}
|
|
210
|
+
translations[language] = {
|
|
211
|
+
text: item.text,
|
|
212
|
+
...(state ? { state: state as TranslationState } : {}),
|
|
213
|
+
...(item.note === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: {
|
|
216
|
+
note: requiredString(
|
|
217
|
+
item.note,
|
|
218
|
+
`${label}.${rawLanguage}.note`,
|
|
219
|
+
),
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return translations;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeEntry(
|
|
227
|
+
value: unknown,
|
|
228
|
+
index: number,
|
|
229
|
+
): ParsedTranslationExchangeEntry {
|
|
230
|
+
const item = record(value, `entries[${index}]`);
|
|
231
|
+
const key = requiredString(item.key, `entries[${index}].key`);
|
|
232
|
+
return {
|
|
233
|
+
key,
|
|
234
|
+
...(item.namespace === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: {
|
|
237
|
+
namespace: requiredString(
|
|
238
|
+
item.namespace,
|
|
239
|
+
`entries[${index}].namespace`,
|
|
240
|
+
),
|
|
241
|
+
}),
|
|
242
|
+
kind: exchangeKind(item.kind, `entries[${index}].kind`),
|
|
243
|
+
format: messageFormat(item.format, `entries[${index}].format`),
|
|
244
|
+
...(item.sourceText === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: {
|
|
247
|
+
sourceText:
|
|
248
|
+
typeof item.sourceText === 'string'
|
|
249
|
+
? item.sourceText
|
|
250
|
+
: requiredString(item.sourceText, `entries[${index}].sourceText`),
|
|
251
|
+
}),
|
|
252
|
+
...(item.keyVersionId === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: {
|
|
255
|
+
keyVersionId: requiredString(
|
|
256
|
+
item.keyVersionId,
|
|
257
|
+
`entries[${index}].keyVersionId`,
|
|
258
|
+
),
|
|
259
|
+
}),
|
|
260
|
+
...(item.sourceHash === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: {
|
|
263
|
+
sourceHash: requiredString(
|
|
264
|
+
item.sourceHash,
|
|
265
|
+
`entries[${index}].sourceHash`,
|
|
266
|
+
),
|
|
267
|
+
}),
|
|
268
|
+
...(item.contractHash === undefined
|
|
269
|
+
? {}
|
|
270
|
+
: {
|
|
271
|
+
contractHash: requiredString(
|
|
272
|
+
item.contractHash,
|
|
273
|
+
`entries[${index}].contractHash`,
|
|
274
|
+
),
|
|
275
|
+
}),
|
|
276
|
+
...(item.argumentSignature === undefined
|
|
277
|
+
? {}
|
|
278
|
+
: {
|
|
279
|
+
argumentSignature: requiredString(
|
|
280
|
+
item.argumentSignature,
|
|
281
|
+
`entries[${index}].argumentSignature`,
|
|
282
|
+
),
|
|
283
|
+
}),
|
|
284
|
+
...(item.description === undefined
|
|
285
|
+
? {}
|
|
286
|
+
: {
|
|
287
|
+
description: requiredString(
|
|
288
|
+
item.description,
|
|
289
|
+
`entries[${index}].description`,
|
|
290
|
+
),
|
|
291
|
+
}),
|
|
292
|
+
translations: normalizeTranslations(
|
|
293
|
+
item.translations,
|
|
294
|
+
`entries[${index}].translations`,
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Normalize adapter output without inventing evidence missing from a foreign
|
|
301
|
+
* file. Import disposition handles those entries as unverified later.
|
|
302
|
+
*/
|
|
303
|
+
export function normalizeParsedTranslationExchangeDocument(
|
|
304
|
+
value: unknown,
|
|
305
|
+
): ParsedTranslationExchangeDocument {
|
|
306
|
+
const input = record(value, 'Translation exchange document');
|
|
307
|
+
if (input.schemaVersion !== 1) {
|
|
308
|
+
throw new Error('Translation exchange schemaVersion must be 1.');
|
|
309
|
+
}
|
|
310
|
+
const sourceLanguage = canonicalLanguageTag(
|
|
311
|
+
requiredString(input.sourceLanguage, 'sourceLanguage'),
|
|
312
|
+
);
|
|
313
|
+
const targetLanguages = canonicalLanguages(
|
|
314
|
+
input.targetLanguages,
|
|
315
|
+
'targetLanguages',
|
|
316
|
+
);
|
|
317
|
+
if (!Array.isArray(input.entries)) {
|
|
318
|
+
throw new Error('entries must be an array.');
|
|
319
|
+
}
|
|
320
|
+
const entries = input.entries.map(normalizeEntry);
|
|
321
|
+
const identities = new Set<string>();
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
for (const language of Object.keys(entry.translations)) {
|
|
324
|
+
const identity = `${entry.namespace ?? ''}\u0000${entry.key}\u0000${language}`;
|
|
325
|
+
if (identities.has(identity)) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Duplicate translation entry for "${entry.key}" in ${language}.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
identities.add(identity);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
schemaVersion: 1,
|
|
335
|
+
...(input.appId === undefined
|
|
336
|
+
? {}
|
|
337
|
+
: { appId: requiredString(input.appId, 'appId') }),
|
|
338
|
+
...(input.branchId === undefined
|
|
339
|
+
? {}
|
|
340
|
+
: { branchId: requiredString(input.branchId, 'branchId') }),
|
|
341
|
+
sourceLanguage,
|
|
342
|
+
targetLanguages,
|
|
343
|
+
...(input.exportedAt === undefined
|
|
344
|
+
? {}
|
|
345
|
+
: { exportedAt: requiredString(input.exportedAt, 'exportedAt') }),
|
|
346
|
+
...(input.revisionWatermark === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: {
|
|
349
|
+
revisionWatermark: requiredString(
|
|
350
|
+
input.revisionWatermark,
|
|
351
|
+
'revisionWatermark',
|
|
352
|
+
),
|
|
353
|
+
}),
|
|
354
|
+
...(input.contractSetHash === undefined
|
|
355
|
+
? {}
|
|
356
|
+
: {
|
|
357
|
+
contractSetHash: requiredString(
|
|
358
|
+
input.contractSetHash,
|
|
359
|
+
'contractSetHash',
|
|
360
|
+
),
|
|
361
|
+
}),
|
|
362
|
+
entries,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Validate the stronger, version-evidenced document required for CN exports. */
|
|
367
|
+
export function validateTranslationExchangeDocument(
|
|
368
|
+
value: unknown,
|
|
369
|
+
): TranslationExchangeDocument {
|
|
370
|
+
const parsed = normalizeParsedTranslationExchangeDocument(value);
|
|
371
|
+
const entries = parsed.entries.map((entry, index): TranslationExchangeEntry => ({
|
|
372
|
+
...entry,
|
|
373
|
+
sourceText:
|
|
374
|
+
entry.sourceText ??
|
|
375
|
+
requiredString(undefined, `entries[${index}].sourceText`),
|
|
376
|
+
keyVersionId:
|
|
377
|
+
entry.keyVersionId ??
|
|
378
|
+
requiredString(undefined, `entries[${index}].keyVersionId`),
|
|
379
|
+
sourceHash:
|
|
380
|
+
entry.sourceHash ??
|
|
381
|
+
requiredString(undefined, `entries[${index}].sourceHash`),
|
|
382
|
+
contractHash:
|
|
383
|
+
entry.contractHash ??
|
|
384
|
+
requiredString(undefined, `entries[${index}].contractHash`),
|
|
385
|
+
argumentSignature:
|
|
386
|
+
entry.argumentSignature ??
|
|
387
|
+
requiredString(undefined, `entries[${index}].argumentSignature`),
|
|
388
|
+
}));
|
|
389
|
+
return { ...parsed, entries };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function isConfirmedBuildDeclaration(
|
|
393
|
+
declaration: TranslationBuildDeclaration,
|
|
394
|
+
): boolean {
|
|
395
|
+
return (
|
|
396
|
+
(declaration.kind ?? 'ui') !== 'content' &&
|
|
397
|
+
(declaration.status ?? 'confirmed') === 'confirmed' &&
|
|
398
|
+
declaration.authoritative !== false
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function registerTranslationFormatAdapter(
|
|
403
|
+
adapter: TranslationFormatAdapter,
|
|
404
|
+
): void {
|
|
405
|
+
if (adapters.has(adapter.format)) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Translation format adapter "${adapter.format}" is already registered.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
adapters.set(adapter.format, adapter);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function getTranslationFormatAdapter(
|
|
414
|
+
format: TranslationFormat,
|
|
415
|
+
): TranslationFormatAdapter | undefined {
|
|
416
|
+
return adapters.get(format);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function listTranslationFormatAdapters(): TranslationFormatAdapter[] {
|
|
420
|
+
return [...adapters.values()].sort((left, right) =>
|
|
421
|
+
left.format.localeCompare(right.format),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Test/host reset hook; adapters are registered explicitly by the host. */
|
|
426
|
+
export function clearTranslationFormatAdapters(): void {
|
|
427
|
+
adapters.clear();
|
|
428
|
+
}
|
package/src/export.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateTranslationExchangeDocument,
|
|
3
|
+
type TranslationExchangeDocument,
|
|
4
|
+
type TranslationExchangeEntry,
|
|
5
|
+
} from './exchange.js';
|
|
6
|
+
import { canonicalLanguageTag, sha256Hex } from './key-version.js';
|
|
7
|
+
import { canonicalJson, translationContractSetHash } from './release.js';
|
|
8
|
+
import type {
|
|
9
|
+
TranslationEntryRecord,
|
|
10
|
+
TranslationState,
|
|
11
|
+
} from './records.js';
|
|
12
|
+
|
|
13
|
+
export interface TranslationExportOptions {
|
|
14
|
+
appId?: string;
|
|
15
|
+
branchId?: string;
|
|
16
|
+
sourceLanguage: string;
|
|
17
|
+
revisionWatermark: string;
|
|
18
|
+
exportedAt?: string;
|
|
19
|
+
languages?: readonly string[];
|
|
20
|
+
namespaces?: readonly string[];
|
|
21
|
+
keys?: readonly string[];
|
|
22
|
+
states?: readonly TranslationState[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TranslationExportFinding {
|
|
26
|
+
key: string;
|
|
27
|
+
code: 'content-key-unsupported' | 'missing-current-version';
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TranslationExportResult {
|
|
32
|
+
document: TranslationExchangeDocument;
|
|
33
|
+
documentHash: string;
|
|
34
|
+
findings: TranslationExportFinding[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build one deterministic, version-evidenced exchange snapshot. Content keys
|
|
39
|
+
* and versionless keys are reported rather than silently losing metadata.
|
|
40
|
+
*/
|
|
41
|
+
export async function createTranslationExchangeDocument(
|
|
42
|
+
entries: readonly TranslationEntryRecord[],
|
|
43
|
+
options: TranslationExportOptions,
|
|
44
|
+
): Promise<TranslationExportResult> {
|
|
45
|
+
const sourceLanguage = canonicalLanguageTag(options.sourceLanguage);
|
|
46
|
+
const languageFilter = options.languages
|
|
47
|
+
? new Set(options.languages.map(canonicalLanguageTag))
|
|
48
|
+
: undefined;
|
|
49
|
+
const namespaceFilter = options.namespaces
|
|
50
|
+
? new Set(options.namespaces)
|
|
51
|
+
: undefined;
|
|
52
|
+
const keyFilter = options.keys ? new Set(options.keys) : undefined;
|
|
53
|
+
const stateFilter = options.states ? new Set(options.states) : undefined;
|
|
54
|
+
const findings: TranslationExportFinding[] = [];
|
|
55
|
+
const exportEntries: TranslationExchangeEntry[] = [];
|
|
56
|
+
|
|
57
|
+
for (const entry of [...entries].sort((left, right) =>
|
|
58
|
+
`${left.namespace}\u0000${left.key}`.localeCompare(
|
|
59
|
+
`${right.namespace}\u0000${right.key}`,
|
|
60
|
+
),
|
|
61
|
+
)) {
|
|
62
|
+
if (namespaceFilter && !namespaceFilter.has(entry.namespace)) continue;
|
|
63
|
+
if (keyFilter && !keyFilter.has(entry.key)) continue;
|
|
64
|
+
if (entry.kind === 'content') {
|
|
65
|
+
findings.push({
|
|
66
|
+
key: entry.key,
|
|
67
|
+
code: 'content-key-unsupported',
|
|
68
|
+
message: 'Content translations are not part of the UI exchange format.',
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const version = entry.currentVersion;
|
|
73
|
+
if (!version) {
|
|
74
|
+
findings.push({
|
|
75
|
+
key: entry.key,
|
|
76
|
+
code: 'missing-current-version',
|
|
77
|
+
message: 'The key has no current immutable version.',
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const translations: TranslationExchangeEntry['translations'] = {};
|
|
82
|
+
for (const [rawLanguage, unit] of Object.entries(entry.units).sort(
|
|
83
|
+
([left], [right]) => left.localeCompare(right),
|
|
84
|
+
)) {
|
|
85
|
+
const language = canonicalLanguageTag(rawLanguage);
|
|
86
|
+
if (language === sourceLanguage) continue;
|
|
87
|
+
if (languageFilter && !languageFilter.has(language)) continue;
|
|
88
|
+
if (stateFilter && !stateFilter.has(unit.state)) continue;
|
|
89
|
+
translations[language] = {
|
|
90
|
+
text: unit.text,
|
|
91
|
+
state: unit.state,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
exportEntries.push({
|
|
95
|
+
key: entry.key,
|
|
96
|
+
namespace: entry.namespace,
|
|
97
|
+
kind:
|
|
98
|
+
entry.ofShape || entry.ofProperty || entry.fromPackage
|
|
99
|
+
? 'semantic'
|
|
100
|
+
: 'ui',
|
|
101
|
+
sourceText: version.sourceText,
|
|
102
|
+
keyVersionId: version.id,
|
|
103
|
+
sourceHash: version.sourceHash,
|
|
104
|
+
contractHash: version.contractHash,
|
|
105
|
+
argumentSignature: version.argumentSignature,
|
|
106
|
+
description: entry.description,
|
|
107
|
+
format: version.format,
|
|
108
|
+
translations,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const targetLanguages = [
|
|
113
|
+
...new Set(
|
|
114
|
+
exportEntries.flatMap((entry) => Object.keys(entry.translations)),
|
|
115
|
+
),
|
|
116
|
+
].sort();
|
|
117
|
+
const document = validateTranslationExchangeDocument({
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
appId: options.appId,
|
|
120
|
+
branchId: options.branchId,
|
|
121
|
+
sourceLanguage,
|
|
122
|
+
targetLanguages,
|
|
123
|
+
exportedAt: options.exportedAt,
|
|
124
|
+
revisionWatermark: options.revisionWatermark,
|
|
125
|
+
contractSetHash: await translationContractSetHash(
|
|
126
|
+
entries.filter((entry) => entry.kind === 'ui') as TranslationEntryRecord[],
|
|
127
|
+
),
|
|
128
|
+
entries: exportEntries,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
document,
|
|
132
|
+
documentHash: await sha256Hex(canonicalJson(document)),
|
|
133
|
+
findings,
|
|
134
|
+
};
|
|
135
|
+
}
|