@_linked/translation 0.9.4 → 0.9.5

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