@_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.
Files changed (153) hide show
  1. package/README.md +102 -0
  2. package/lib/cjs/data/translation.json +8 -0
  3. package/lib/esm/archive.d.ts +133 -0
  4. package/lib/esm/archive.js +567 -0
  5. package/lib/esm/archive.js.map +1 -0
  6. package/lib/esm/authorization.d.ts +12 -0
  7. package/lib/esm/authorization.js +10 -0
  8. package/lib/esm/authorization.js.map +1 -0
  9. package/lib/esm/backend.d.ts +1 -0
  10. package/lib/esm/backend.js +3 -0
  11. package/lib/esm/backend.js.map +1 -0
  12. package/lib/esm/cdn-loader.d.ts +40 -0
  13. package/lib/esm/cdn-loader.js +200 -0
  14. package/lib/esm/cdn-loader.js.map +1 -0
  15. package/lib/esm/checks.d.ts +56 -0
  16. package/lib/esm/checks.js +288 -0
  17. package/lib/esm/checks.js.map +1 -0
  18. package/lib/esm/compile.d.ts +64 -0
  19. package/lib/esm/compile.js +99 -0
  20. package/lib/esm/compile.js.map +1 -0
  21. package/lib/esm/core/messages.d.ts +25 -0
  22. package/lib/esm/core/messages.js +76 -0
  23. package/lib/esm/core/messages.js.map +1 -0
  24. package/lib/esm/data/translation.json +8 -0
  25. package/lib/esm/discovery.d.ts +97 -0
  26. package/lib/esm/discovery.js +365 -0
  27. package/lib/esm/discovery.js.map +1 -0
  28. package/lib/esm/exchange.d.ts +100 -0
  29. package/lib/esm/exchange.js +223 -0
  30. package/lib/esm/exchange.js.map +1 -0
  31. package/lib/esm/export.d.ts +28 -0
  32. package/lib/esm/export.js +92 -0
  33. package/lib/esm/export.js.map +1 -0
  34. package/lib/esm/formats/csv.d.ts +8 -0
  35. package/lib/esm/formats/csv.js +397 -0
  36. package/lib/esm/formats/csv.js.map +1 -0
  37. package/lib/esm/formats/json.d.ts +30 -0
  38. package/lib/esm/formats/json.js +459 -0
  39. package/lib/esm/formats/json.js.map +1 -0
  40. package/lib/esm/formats/xliff.d.ts +12 -0
  41. package/lib/esm/formats/xliff.js +577 -0
  42. package/lib/esm/formats/xliff.js.map +1 -0
  43. package/lib/esm/import.d.ts +197 -0
  44. package/lib/esm/import.js +391 -0
  45. package/lib/esm/import.js.map +1 -0
  46. package/lib/esm/index.d.ts +42 -0
  47. package/lib/esm/index.js +34 -0
  48. package/lib/esm/index.js.map +1 -0
  49. package/lib/esm/key-sync-node.d.ts +29 -0
  50. package/lib/esm/key-sync-node.js +174 -0
  51. package/lib/esm/key-sync-node.js.map +1 -0
  52. package/lib/esm/key-sync.d.ts +46 -0
  53. package/lib/esm/key-sync.js +114 -0
  54. package/lib/esm/key-sync.js.map +1 -0
  55. package/lib/esm/key-version.d.ts +50 -0
  56. package/lib/esm/key-version.js +134 -0
  57. package/lib/esm/key-version.js.map +1 -0
  58. package/lib/esm/languages.d.ts +18 -0
  59. package/lib/esm/languages.js +74 -0
  60. package/lib/esm/languages.js.map +1 -0
  61. package/lib/esm/linked-discovery.d.ts +44 -0
  62. package/lib/esm/linked-discovery.js +267 -0
  63. package/lib/esm/linked-discovery.js.map +1 -0
  64. package/lib/esm/message-format.d.ts +41 -0
  65. package/lib/esm/message-format.js +133 -0
  66. package/lib/esm/message-format.js.map +1 -0
  67. package/lib/esm/mt.d.ts +111 -0
  68. package/lib/esm/mt.js +142 -0
  69. package/lib/esm/mt.js.map +1 -0
  70. package/lib/esm/ontologies/translation.d.ts +203 -0
  71. package/lib/esm/ontologies/translation.js +219 -0
  72. package/lib/esm/ontologies/translation.js.map +1 -0
  73. package/lib/esm/package.d.ts +4 -0
  74. package/lib/esm/package.js +11 -0
  75. package/lib/esm/package.js.map +1 -0
  76. package/lib/esm/react.d.ts +55 -0
  77. package/lib/esm/react.js +127 -0
  78. package/lib/esm/react.js.map +1 -0
  79. package/lib/esm/records.d.ts +151 -0
  80. package/lib/esm/records.js +2 -0
  81. package/lib/esm/records.js.map +1 -0
  82. package/lib/esm/release.d.ts +198 -0
  83. package/lib/esm/release.js +435 -0
  84. package/lib/esm/release.js.map +1 -0
  85. package/lib/esm/shape-catalog.d.ts +79 -0
  86. package/lib/esm/shape-catalog.js +118 -0
  87. package/lib/esm/shape-catalog.js.map +1 -0
  88. package/lib/esm/shapes/GlossaryTerm.d.ts +28 -0
  89. package/lib/esm/shapes/GlossaryTerm.js +94 -0
  90. package/lib/esm/shapes/GlossaryTerm.js.map +1 -0
  91. package/lib/esm/shapes/TranslationInventoryOrigin.d.ts +33 -0
  92. package/lib/esm/shapes/TranslationInventoryOrigin.js +209 -0
  93. package/lib/esm/shapes/TranslationInventoryOrigin.js.map +1 -0
  94. package/lib/esm/shapes/TranslationKey.d.ts +92 -0
  95. package/lib/esm/shapes/TranslationKey.js +194 -0
  96. package/lib/esm/shapes/TranslationKey.js.map +1 -0
  97. package/lib/esm/shapes/TranslationKeyVersion.d.ts +19 -0
  98. package/lib/esm/shapes/TranslationKeyVersion.js +125 -0
  99. package/lib/esm/shapes/TranslationKeyVersion.js.map +1 -0
  100. package/lib/esm/shapes/TranslationLanguage.d.ts +15 -0
  101. package/lib/esm/shapes/TranslationLanguage.js +102 -0
  102. package/lib/esm/shapes/TranslationLanguage.js.map +1 -0
  103. package/lib/esm/shapes/TranslationProvider.d.ts +409 -0
  104. package/lib/esm/shapes/TranslationProvider.js +1376 -0
  105. package/lib/esm/shapes/TranslationProvider.js.map +1 -0
  106. package/lib/esm/shapes/TranslationRelease.d.ts +36 -0
  107. package/lib/esm/shapes/TranslationRelease.js +149 -0
  108. package/lib/esm/shapes/TranslationRelease.js.map +1 -0
  109. package/lib/esm/shapes/TranslationRevision.d.ts +39 -0
  110. package/lib/esm/shapes/TranslationRevision.js +159 -0
  111. package/lib/esm/shapes/TranslationRevision.js.map +1 -0
  112. package/lib/esm/shapes/TranslationUnit.d.ts +26 -0
  113. package/lib/esm/shapes/TranslationUnit.js +111 -0
  114. package/lib/esm/shapes/TranslationUnit.js.map +1 -0
  115. package/package.json +134 -0
  116. package/src/archive.ts +894 -0
  117. package/src/authorization.ts +33 -0
  118. package/src/backend.ts +2 -0
  119. package/src/cdn-loader.ts +264 -0
  120. package/src/checks.ts +413 -0
  121. package/src/compile.ts +168 -0
  122. package/src/core/messages.ts +106 -0
  123. package/src/data/translation.json +8 -0
  124. package/src/discovery.ts +594 -0
  125. package/src/exchange.ts +428 -0
  126. package/src/export.ts +135 -0
  127. package/src/formats/csv.ts +459 -0
  128. package/src/formats/json.ts +587 -0
  129. package/src/formats/xliff.ts +708 -0
  130. package/src/import.ts +648 -0
  131. package/src/index.ts +207 -0
  132. package/src/key-sync-node.ts +269 -0
  133. package/src/key-sync.ts +168 -0
  134. package/src/key-version.ts +206 -0
  135. package/src/languages.ts +102 -0
  136. package/src/linked-discovery.ts +385 -0
  137. package/src/message-format.ts +214 -0
  138. package/src/mt.ts +248 -0
  139. package/src/ontologies/translation.ts +234 -0
  140. package/src/package.ts +19 -0
  141. package/src/react.tsx +250 -0
  142. package/src/records.ts +180 -0
  143. package/src/release.ts +726 -0
  144. package/src/shape-catalog.ts +198 -0
  145. package/src/shapes/GlossaryTerm.ts +61 -0
  146. package/src/shapes/TranslationInventoryOrigin.ts +131 -0
  147. package/src/shapes/TranslationKey.ts +174 -0
  148. package/src/shapes/TranslationKeyVersion.ts +77 -0
  149. package/src/shapes/TranslationLanguage.ts +51 -0
  150. package/src/shapes/TranslationProvider.ts +1755 -0
  151. package/src/shapes/TranslationRelease.ts +114 -0
  152. package/src/shapes/TranslationRevision.ts +105 -0
  153. package/src/shapes/TranslationUnit.ts +72 -0
@@ -0,0 +1,1376 @@
1
+ import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
2
+ import { TranslationKey } from './TranslationKey.js';
3
+ import { TranslationKeyVersion } from './TranslationKeyVersion.js';
4
+ import { TranslationRelease } from './TranslationRelease.js';
5
+ import { TranslationUnit } from './TranslationUnit.js';
6
+ import { TranslationRevision } from './TranslationRevision.js';
7
+ import { GlossaryTerm } from './GlossaryTerm.js';
8
+ import { classifyKeyVersion, createMonotonicVersionId, deriveBackfillKeyVersion, deriveTranslationKeyVersionContract, } from '../key-version.js';
9
+ import { canAuthorTranslation, } from '../authorization.js';
10
+ const iri = (value) => typeof value === 'string'
11
+ ? value
12
+ : value === null || value === void 0 ? void 0 : value.id;
13
+ const isoDate = (value) => value instanceof Date ? value.toISOString() : value || undefined;
14
+ function toKeyVersionRecord(row) {
15
+ var _a, _b;
16
+ const id = (_a = iri(row.id)) !== null && _a !== void 0 ? _a : iri(row);
17
+ if (!id || !row.versionId)
18
+ return null;
19
+ return {
20
+ id,
21
+ versionId: row.versionId,
22
+ sourceLanguage: row.sourceLanguage || 'en',
23
+ sourceText: (_b = row.sourceText) !== null && _b !== void 0 ? _b : '',
24
+ format: row.format === 'icu' ? 'icu' : 'simple',
25
+ argumentSignature: row.argumentSignature || '[]',
26
+ contractHash: row.contractHash || '',
27
+ sourceHash: row.sourceHash || '',
28
+ supersedes: iri(row.supersedes),
29
+ createdAt: isoDate(row.createdAt) || undefined,
30
+ createdBy: iri(row.createdBy),
31
+ };
32
+ }
33
+ /**
34
+ * Shared TM/carry-forward classifier (Plan 018 §5.5). Exact source matches may
35
+ * cross logical keys; changed-source carry-forward candidates never do.
36
+ */
37
+ export function classifyTranslationMemoryMatches(input) {
38
+ var _a, _b, _c, _d, _e;
39
+ const matches = [];
40
+ for (const row of (_a = input.rows) !== null && _a !== void 0 ? _a : []) {
41
+ const version = typeof row.ofKeyVersion === 'object' ? row.ofKeyVersion : undefined;
42
+ const unitId = iri(row.id);
43
+ const keyVersionId = iri(row.ofKeyVersion);
44
+ const candidateKey = (_b = row.ofKey) === null || _b === void 0 ? void 0 : _b.key;
45
+ if (!unitId ||
46
+ !keyVersionId ||
47
+ keyVersionId === input.currentVersion.id ||
48
+ !candidateKey ||
49
+ row.language !== input.language ||
50
+ row.state !== 'reviewed' ||
51
+ !((_c = row.text) === null || _c === void 0 ? void 0 : _c.trim()) ||
52
+ !(version === null || version === void 0 ? void 0 : version.contractHash) ||
53
+ version.contractHash !== input.currentVersion.contractHash) {
54
+ continue;
55
+ }
56
+ const exact = version.sourceHash === input.currentVersion.sourceHash;
57
+ if (!exact && candidateKey !== input.key)
58
+ continue;
59
+ matches.push({
60
+ unitId,
61
+ key: candidateKey,
62
+ language: input.language,
63
+ text: row.text,
64
+ keyVersionId,
65
+ sourceText: (_d = version.sourceText) !== null && _d !== void 0 ? _d : '',
66
+ sourceHash: (_e = version.sourceHash) !== null && _e !== void 0 ? _e : '',
67
+ contractHash: version.contractHash,
68
+ kind: exact ? 'exact' : 'carry-forward',
69
+ updatedAt: isoDate(row.updatedAt) || undefined,
70
+ });
71
+ }
72
+ return matches.sort((left, right) => {
73
+ var _a, _b;
74
+ return (left.kind === right.kind ? 0 : left.kind === 'exact' ? -1 : 1) ||
75
+ ((_a = right.updatedAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = left.updatedAt) !== null && _b !== void 0 ? _b : '') ||
76
+ left.key.localeCompare(right.key);
77
+ });
78
+ }
79
+ /** Build the reviewed-unit index shown in the explicit TM browser. */
80
+ export function toTranslationMemoryRecords(rows = []) {
81
+ return rows
82
+ .flatMap((row) => {
83
+ var _a, _b, _c;
84
+ const version = typeof row.ofKeyVersion === 'object' ? row.ofKeyVersion : undefined;
85
+ const unitId = iri(row.id);
86
+ const keyVersionId = iri(row.ofKeyVersion);
87
+ const key = (_a = row.ofKey) === null || _a === void 0 ? void 0 : _a.key;
88
+ if (!unitId ||
89
+ !keyVersionId ||
90
+ !key ||
91
+ !row.language ||
92
+ row.state !== 'reviewed' ||
93
+ !((_b = row.text) === null || _b === void 0 ? void 0 : _b.trim()) ||
94
+ !(version === null || version === void 0 ? void 0 : version.sourceHash) ||
95
+ !version.contractHash) {
96
+ return [];
97
+ }
98
+ return [
99
+ {
100
+ unitId,
101
+ key,
102
+ language: row.language,
103
+ text: row.text,
104
+ keyVersionId,
105
+ sourceText: (_c = version.sourceText) !== null && _c !== void 0 ? _c : '',
106
+ sourceHash: version.sourceHash,
107
+ contractHash: version.contractHash,
108
+ updatedAt: isoDate(row.updatedAt) || undefined,
109
+ },
110
+ ];
111
+ })
112
+ .sort((left, right) => {
113
+ var _a, _b;
114
+ return ((_a = right.updatedAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = left.updatedAt) !== null && _b !== void 0 ? _b : '') ||
115
+ left.language.localeCompare(right.language) ||
116
+ left.key.localeCompare(right.key);
117
+ });
118
+ }
119
+ /**
120
+ * Find current untranslated cells that can be filled without MT cost.
121
+ * Source AND argument-contract hashes must match; the current version can
122
+ * never source itself.
123
+ */
124
+ export function findExactMemoryPretranslations(input) {
125
+ var _a;
126
+ const byContract = new Map();
127
+ for (const record of input.memory) {
128
+ if (record.language !== input.language)
129
+ continue;
130
+ const signature = `${record.sourceHash}\u0000${record.contractHash}`;
131
+ const candidates = (_a = byContract.get(signature)) !== null && _a !== void 0 ? _a : [];
132
+ candidates.push(record);
133
+ byContract.set(signature, candidates);
134
+ }
135
+ for (const candidates of byContract.values()) {
136
+ candidates.sort((left, right) => {
137
+ var _a, _b;
138
+ return ((_a = right.updatedAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = left.updatedAt) !== null && _b !== void 0 ? _b : '') ||
139
+ left.key.localeCompare(right.key);
140
+ });
141
+ }
142
+ return input.entries.flatMap((entry) => {
143
+ var _a;
144
+ const current = entry.currentVersion;
145
+ const unit = entry.units[input.language];
146
+ if (!(current === null || current === void 0 ? void 0 : current.sourceHash) ||
147
+ !current.contractHash ||
148
+ (unit === null || unit === void 0 ? void 0 : unit.text.trim())) {
149
+ return [];
150
+ }
151
+ const signature = `${current.sourceHash}\u0000${current.contractHash}`;
152
+ const match = (_a = byContract
153
+ .get(signature)) === null || _a === void 0 ? void 0 : _a.find((candidate) => candidate.keyVersionId !== current.id);
154
+ return match
155
+ ? [
156
+ {
157
+ key: entry.key,
158
+ language: input.language,
159
+ unitId: match.unitId,
160
+ sourceKey: match.key,
161
+ text: match.text,
162
+ },
163
+ ]
164
+ : [];
165
+ });
166
+ }
167
+ const TRANSLATION_STATES = new Set([
168
+ 'untranslated',
169
+ 'machine',
170
+ 'reviewed',
171
+ 'stale',
172
+ ]);
173
+ function normalizeState(value) {
174
+ return TRANSLATION_STATES.has(value)
175
+ ? value
176
+ : 'untranslated';
177
+ }
178
+ /**
179
+ * Convert LINKED query rows into a deterministic, serialization-safe editor
180
+ * contract. Exported for package-unit testing; it performs no I/O.
181
+ */
182
+ export function groupTranslationEntries(keyRows = [], unitRows = [], versionRows = []) {
183
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
184
+ const entries = new Map();
185
+ const currentVersionByKey = new Map();
186
+ for (const row of keyRows !== null && keyRows !== void 0 ? keyRows : []) {
187
+ if (!(row === null || row === void 0 ? void 0 : row.key))
188
+ continue;
189
+ const currentVersionId = iri(row.currentVersion);
190
+ if (currentVersionId)
191
+ currentVersionByKey.set(row.key, currentVersionId);
192
+ entries.set(row.key, {
193
+ key: row.key,
194
+ namespace: (_a = row.namespace) !== null && _a !== void 0 ? _a : '',
195
+ sourceText: (_b = row.sourceText) !== null && _b !== void 0 ? _b : '',
196
+ description: row.description || undefined,
197
+ kind: row.kind === 'content' ? 'content' : 'ui',
198
+ format: row.format === 'icu' ? 'icu' : 'simple',
199
+ ofShape: iri(row.ofShape),
200
+ ofProperty: iri(row.ofProperty),
201
+ fromPackage: row.fromPackage || undefined,
202
+ overridden: row.overridden === true || row.overridden === 'true'
203
+ ? true
204
+ : undefined,
205
+ versions: [],
206
+ units: {},
207
+ });
208
+ }
209
+ for (const row of versionRows !== null && versionRows !== void 0 ? versionRows : []) {
210
+ const key = (_c = row === null || row === void 0 ? void 0 : row.ofKey) === null || _c === void 0 ? void 0 : _c.key;
211
+ const entry = key ? entries.get(key) : undefined;
212
+ const version = toKeyVersionRecord(row);
213
+ if (!entry || !version)
214
+ continue;
215
+ entry.versions.push(version);
216
+ if (currentVersionByKey.get(key) === version.id) {
217
+ entry.currentVersion = version;
218
+ }
219
+ }
220
+ for (const entry of entries.values()) {
221
+ (_d = entry.versions) === null || _d === void 0 ? void 0 : _d.sort((left, right) => {
222
+ var _a, _b;
223
+ return ((_a = right.createdAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = left.createdAt) !== null && _b !== void 0 ? _b : '') ||
224
+ right.versionId.localeCompare(left.versionId);
225
+ });
226
+ if (!((_e = entry.versions) === null || _e === void 0 ? void 0 : _e.length))
227
+ delete entry.versions;
228
+ }
229
+ const unitCandidates = new Map();
230
+ for (const row of unitRows !== null && unitRows !== void 0 ? unitRows : []) {
231
+ const key = (_f = row === null || row === void 0 ? void 0 : row.ofKey) === null || _f === void 0 ? void 0 : _f.key;
232
+ const language = row === null || row === void 0 ? void 0 : row.language;
233
+ const entry = key ? entries.get(key) : undefined;
234
+ if (!entry || !language)
235
+ continue;
236
+ const keyVersionId = iri(row.ofKeyVersion);
237
+ const score = keyVersionId && keyVersionId === ((_g = entry.currentVersion) === null || _g === void 0 ? void 0 : _g.id)
238
+ ? 2
239
+ : keyVersionId
240
+ ? 1
241
+ : 0;
242
+ const candidateKey = `${key}\u0000${language}`;
243
+ if (((_j = (_h = unitCandidates.get(candidateKey)) === null || _h === void 0 ? void 0 : _h.score) !== null && _j !== void 0 ? _j : -1) >= score)
244
+ continue;
245
+ unitCandidates.set(candidateKey, {
246
+ score,
247
+ record: {
248
+ language,
249
+ text: (_k = row.text) !== null && _k !== void 0 ? _k : '',
250
+ state: entry.currentVersion && score < 2
251
+ ? 'stale'
252
+ : normalizeState(row.state),
253
+ origin: row.origin || undefined,
254
+ updatedAt: isoDate(row.updatedAt) || undefined,
255
+ keyVersionId,
256
+ },
257
+ });
258
+ }
259
+ for (const [candidateKey, { record }] of unitCandidates) {
260
+ const [key, language] = candidateKey.split('\u0000');
261
+ const entry = entries.get(key);
262
+ if (entry)
263
+ entry.units[language] = record;
264
+ }
265
+ return [...entries.values()].sort((a, b) => a.key.localeCompare(b.key));
266
+ }
267
+ /** App-scoped key read shared by the RPC provider and managed key-sync flows. */
268
+ export async function listTranslationKeys(data) {
269
+ const appId = requireText(data.appId, 'appId');
270
+ return TranslationKey.select((k) => [
271
+ k.namespace,
272
+ k.key,
273
+ k.sourceText,
274
+ k.description,
275
+ k.kind,
276
+ k.ofNode,
277
+ k.ofField,
278
+ k.ofShape,
279
+ k.ofProperty,
280
+ k.fromPackage,
281
+ k.overridden,
282
+ k.format,
283
+ k.currentVersion,
284
+ ]).where((k) => k.ofApplication.equals({ id: appId }));
285
+ }
286
+ /**
287
+ * App-scoped keys + grouped language units as plain records — the read behind
288
+ * the editor RPC AND the publish/compile flow (Plan 016 P2.3). No auth of its
289
+ * own (the RPC wrapper gates); callers on the server run it in request context.
290
+ */
291
+ export async function listTranslationEntries(data) {
292
+ const appId = requireText(data.appId, 'appId');
293
+ const [keys, units, versions] = await Promise.all([
294
+ TranslationKey.select((k) => [
295
+ k.namespace,
296
+ k.key,
297
+ k.sourceText,
298
+ k.description,
299
+ k.kind,
300
+ k.format,
301
+ k.ofShape,
302
+ k.ofProperty,
303
+ k.fromPackage,
304
+ k.overridden,
305
+ k.currentVersion,
306
+ ]).where((k) => k.ofApplication.equals({ id: appId })),
307
+ TranslationUnit.select((u) => [
308
+ u.language,
309
+ u.text,
310
+ u.state,
311
+ u.origin,
312
+ u.updatedAt,
313
+ u.ofKeyVersion,
314
+ u.ofKey.select((k) => [k.key]),
315
+ ]).where((u) => u.ofKey.ofApplication.equals({ id: appId })),
316
+ TranslationKeyVersion.select((v) => [
317
+ v.versionId,
318
+ v.sourceLanguage,
319
+ v.sourceText,
320
+ v.format,
321
+ v.argumentSignature,
322
+ v.contractHash,
323
+ v.sourceHash,
324
+ v.supersedes,
325
+ v.createdAt,
326
+ v.createdBy,
327
+ v.ofKey.select((k) => [k.key]),
328
+ ]).where((v) => v.ofApplication.equals({ id: appId })),
329
+ ]);
330
+ return groupTranslationEntries(keys, units, versions);
331
+ }
332
+ export async function listTranslationKeyVersions(data) {
333
+ const appId = requireText(data.appId, 'appId');
334
+ const keyId = data.key
335
+ ? keyIri(appId, requireText(data.key, 'key'))
336
+ : undefined;
337
+ const query = TranslationKeyVersion.select((v) => [
338
+ v.versionId,
339
+ v.sourceLanguage,
340
+ v.sourceText,
341
+ v.format,
342
+ v.argumentSignature,
343
+ v.contractHash,
344
+ v.sourceHash,
345
+ v.supersedes,
346
+ v.createdAt,
347
+ v.createdBy,
348
+ ]).where((v) => keyId
349
+ ? v.ofApplication
350
+ .equals({ id: appId })
351
+ .and(v.ofKey.equals({ id: keyId }))
352
+ : v.ofApplication.equals({ id: appId }));
353
+ const rows = await query;
354
+ return (rows !== null && rows !== void 0 ? rows : [])
355
+ .map((row) => toKeyVersionRecord(row))
356
+ .filter((record) => record !== null)
357
+ .sort((left, right) => {
358
+ var _a, _b;
359
+ return ((_a = right.createdAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = left.createdAt) !== null && _b !== void 0 ? _b : '') ||
360
+ right.versionId.localeCompare(left.versionId);
361
+ });
362
+ }
363
+ function toReleaseRecord(row) {
364
+ var _a, _b;
365
+ const id = (_a = iri(row === null || row === void 0 ? void 0 : row.id)) !== null && _a !== void 0 ? _a : iri(row);
366
+ const appId = iri(row === null || row === void 0 ? void 0 : row.ofApplication);
367
+ const branchId = iri(row === null || row === void 0 ? void 0 : row.branch);
368
+ if (!id || !appId || !branchId || !(row === null || row === void 0 ? void 0 : row.releaseId))
369
+ return null;
370
+ return {
371
+ id,
372
+ releaseId: row.releaseId,
373
+ appId,
374
+ branchId,
375
+ buildId: row.buildId || undefined,
376
+ channel: row.channel === 'production' ? 'production' : 'preview',
377
+ status: ['published', 'superseded', 'retired'].includes(row.status)
378
+ ? row.status
379
+ : 'draft',
380
+ contractSetHash: row.contractSetHash || '',
381
+ manifestHash: row.manifestHash || '',
382
+ hotfixSequence: Number((_b = row.hotfixSequence) !== null && _b !== void 0 ? _b : 0),
383
+ qualitySummary: row.qualitySummary || undefined,
384
+ createdAt: isoDate(row.createdAt) || '',
385
+ publishedAt: isoDate(row.publishedAt) || undefined,
386
+ supportedUntil: isoDate(row.supportedUntil) || undefined,
387
+ supersedes: iri(row.supersedes),
388
+ };
389
+ }
390
+ export async function listTranslationReleases(data) {
391
+ const appId = requireText(data.appId, 'appId');
392
+ const rows = await TranslationRelease.select((release) => [
393
+ release.releaseId,
394
+ release.ofApplication,
395
+ release.branch,
396
+ release.buildId,
397
+ release.channel,
398
+ release.status,
399
+ release.contractSetHash,
400
+ release.manifestHash,
401
+ release.hotfixSequence,
402
+ release.qualitySummary,
403
+ release.createdAt,
404
+ release.publishedAt,
405
+ release.supportedUntil,
406
+ release.supersedes,
407
+ ]).where((release) => release.ofApplication.equals({ id: appId }));
408
+ return (rows !== null && rows !== void 0 ? rows : [])
409
+ .map(toReleaseRecord)
410
+ .filter((record) => record !== null)
411
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
412
+ }
413
+ export async function createTranslationRelease(data) {
414
+ const appId = requireText(data.appId, 'appId');
415
+ const releaseId = requireText(data.releaseId, 'releaseId');
416
+ const branchId = requireText(data.branchId, 'branchId');
417
+ if (!['preview', 'production'].includes(data.channel)) {
418
+ throw new Error('channel must be preview or production.');
419
+ }
420
+ if (!['draft', 'published', 'superseded', 'retired'].includes(data.status)) {
421
+ throw new Error('Invalid translation release status.');
422
+ }
423
+ const id = `${appId.replace(/\/$/, '')}/translation/release/${encodeURIComponent(releaseId)}`;
424
+ const existing = await TranslationRelease.select((release) => [
425
+ release.releaseId,
426
+ ])
427
+ .where((release) => release.equals({ id }))
428
+ .one()
429
+ .catch(() => null);
430
+ if (existing) {
431
+ throw new Error(`Translation release "${releaseId}" already exists.`);
432
+ }
433
+ const createdAt = data.createdAt || new Date().toISOString();
434
+ await TranslationRelease.create({
435
+ __id: id,
436
+ ofApplication: { id: appId },
437
+ branch: { id: branchId },
438
+ releaseId,
439
+ buildId: data.buildId,
440
+ channel: data.channel,
441
+ status: data.status,
442
+ contractSetHash: requireText(data.contractSetHash, 'contractSetHash'),
443
+ manifestHash: requireText(data.manifestHash, 'manifestHash'),
444
+ hotfixSequence: data.hotfixSequence,
445
+ qualitySummary: data.qualitySummary,
446
+ createdAt: new Date(createdAt),
447
+ publishedAt: data.publishedAt ? new Date(data.publishedAt) : undefined,
448
+ supportedUntil: data.supportedUntil ? new Date(data.supportedUntil) : undefined,
449
+ supersedes: data.supersedes ? { id: data.supersedes } : undefined,
450
+ });
451
+ return { ...data, id, createdAt };
452
+ }
453
+ export async function advanceTranslationReleaseHotfix(data) {
454
+ const id = requireText(data.id, 'id');
455
+ const current = await TranslationRelease.select((release) => [
456
+ release.releaseId,
457
+ release.ofApplication,
458
+ release.branch,
459
+ release.buildId,
460
+ release.channel,
461
+ release.status,
462
+ release.contractSetHash,
463
+ release.manifestHash,
464
+ release.hotfixSequence,
465
+ release.qualitySummary,
466
+ release.createdAt,
467
+ release.publishedAt,
468
+ release.supportedUntil,
469
+ release.supersedes,
470
+ ])
471
+ .where((release) => release.equals({ id }))
472
+ .one()
473
+ .catch(() => null);
474
+ const record = current ? toReleaseRecord(current) : null;
475
+ if (!record)
476
+ throw new Error(`Translation release "${id}" was not found.`);
477
+ if (record.hotfixSequence !== data.expectedSequence) {
478
+ throw new Error(`Translation release advanced from sequence ${data.expectedSequence} to ${record.hotfixSequence}.`);
479
+ }
480
+ await TranslationRelease.update({
481
+ hotfixSequence: data.hotfixSequence,
482
+ manifestHash: requireText(data.manifestHash, 'manifestHash'),
483
+ }).for({ id });
484
+ return {
485
+ ...record,
486
+ hotfixSequence: data.hotfixSequence,
487
+ manifestHash: data.manifestHash,
488
+ };
489
+ }
490
+ function requireText(value, label) {
491
+ const clean = value === null || value === void 0 ? void 0 : value.trim();
492
+ if (!clean)
493
+ throw new Error(`${label} is required.`);
494
+ return clean;
495
+ }
496
+ function keyIri(appId, key) {
497
+ return `${appId.replace(/\/$/, '')}/translation/key/${encodeURIComponent(key)}`;
498
+ }
499
+ function versionIri(keyId, versionId) {
500
+ return `${keyId.replace(/\/$/, '')}/version/${versionId}`;
501
+ }
502
+ function unitIri(keyVersionId, language) {
503
+ return `${keyVersionId.replace(/\/$/, '')}/unit/${encodeURIComponent(language)}`;
504
+ }
505
+ /** Unique append-only revision IRI under the version-scoped cell it revises. */
506
+ function revisionIri(keyVersionId, language) {
507
+ const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
508
+ return `${unitIri(keyVersionId, language)}/rev/${suffix}`;
509
+ }
510
+ async function readKeyVersion(id) {
511
+ const row = await TranslationKeyVersion.select((v) => [
512
+ v.versionId,
513
+ v.sourceLanguage,
514
+ v.sourceText,
515
+ v.format,
516
+ v.argumentSignature,
517
+ v.contractHash,
518
+ v.sourceHash,
519
+ v.supersedes,
520
+ v.createdAt,
521
+ v.createdBy,
522
+ ])
523
+ .where((v) => v.equals({ id }))
524
+ .one()
525
+ .catch(() => null);
526
+ return row ? toKeyVersionRecord(row) : null;
527
+ }
528
+ async function createTranslationKeyVersion(input) {
529
+ const derived = input.backfill
530
+ ? await deriveBackfillKeyVersion(input)
531
+ : {
532
+ ...(await deriveTranslationKeyVersionContract(input)),
533
+ versionId: createMonotonicVersionId(),
534
+ };
535
+ const id = versionIri(input.keyId, derived.versionId);
536
+ const createdAt = new Date().toISOString();
537
+ await TranslationKeyVersion.create({
538
+ __id: id,
539
+ ofApplication: { id: input.appId },
540
+ ofKey: { id: input.keyId },
541
+ versionId: derived.versionId,
542
+ sourceLanguage: derived.sourceLanguage,
543
+ sourceText: derived.sourceText,
544
+ format: derived.format,
545
+ argumentSignature: derived.argumentSignature,
546
+ contractHash: derived.contractHash,
547
+ sourceHash: derived.sourceHash,
548
+ supersedes: input.supersedes ? { id: input.supersedes } : undefined,
549
+ createdAt: new Date(createdAt),
550
+ createdBy: input.createdBy ? { id: input.createdBy } : undefined,
551
+ });
552
+ return {
553
+ id,
554
+ ...derived,
555
+ supersedes: input.supersedes,
556
+ createdAt,
557
+ createdBy: input.createdBy,
558
+ };
559
+ }
560
+ async function ensureCurrentKeyVersion(input) {
561
+ const currentId = iri(input.currentVersion);
562
+ if (currentId) {
563
+ const current = await readKeyVersion(currentId);
564
+ if (current)
565
+ return current;
566
+ }
567
+ const initial = await createTranslationKeyVersion({
568
+ ...input,
569
+ backfill: true,
570
+ });
571
+ await TranslationKey.update({
572
+ currentVersion: { id: initial.id },
573
+ }).for({ id: input.keyId });
574
+ return initial;
575
+ }
576
+ const REVISION_STATUSES = new Set([
577
+ 'applied',
578
+ 'proposed',
579
+ 'suggested',
580
+ 'accepted',
581
+ 'rejected',
582
+ 'superseded',
583
+ ]);
584
+ async function requireKeyWriteContext(input) {
585
+ var _a;
586
+ const keyId = keyIri(input.appId, input.key);
587
+ const keyRow = await TranslationKey.select((k) => [
588
+ k.sourceText,
589
+ k.format,
590
+ k.currentVersion,
591
+ ])
592
+ .where((k) => k.equals({ id: keyId }))
593
+ .one()
594
+ .catch(() => null);
595
+ if (!keyRow) {
596
+ throw new Error(`Translation key "${input.key}" does not exist for this app.`);
597
+ }
598
+ const version = await ensureCurrentKeyVersion({
599
+ appId: input.appId,
600
+ keyId,
601
+ currentVersion: keyRow.currentVersion,
602
+ sourceLanguage: 'en',
603
+ sourceText: (_a = keyRow.sourceText) !== null && _a !== void 0 ? _a : '',
604
+ format: keyRow.format === 'icu' ? 'icu' : 'simple',
605
+ createdBy: input.createdBy,
606
+ });
607
+ return { keyId, version };
608
+ }
609
+ /**
610
+ * Convert LINKED revision rows into plain RPC-safe records, newest first.
611
+ * Exported for package-unit testing; performs no I/O.
612
+ */
613
+ export function toRevisionRecords(rows = []) {
614
+ const iri = (value) => typeof value === 'string'
615
+ ? value
616
+ : value === null || value === void 0 ? void 0 : value.id;
617
+ return rows
618
+ .filter((row) => typeof (row === null || row === void 0 ? void 0 : row.text) === 'string')
619
+ .map((row) => {
620
+ var _a, _b, _c, _d, _e, _f;
621
+ return ({
622
+ id: (_b = (_a = iri(row.id)) !== null && _a !== void 0 ? _a : iri(row)) !== null && _b !== void 0 ? _b : '',
623
+ key: (_d = (_c = row.ofKey) === null || _c === void 0 ? void 0 : _c.key) !== null && _d !== void 0 ? _d : '',
624
+ language: (_e = row.language) !== null && _e !== void 0 ? _e : '',
625
+ text: (_f = row.text) !== null && _f !== void 0 ? _f : '',
626
+ status: REVISION_STATUSES.has(row.status)
627
+ ? row.status
628
+ : 'applied',
629
+ author: iri(row.author),
630
+ authorKind: row.authorKind === 'machine' ? 'machine' : 'human',
631
+ mtProvider: row.mtProvider || undefined,
632
+ basedOnText: typeof row.basedOnText === 'string' ? row.basedOnText : undefined,
633
+ note: row.note || undefined,
634
+ createdAt: isoDate(row.createdAt) || undefined,
635
+ decidedAt: isoDate(row.decidedAt) || undefined,
636
+ decidedBy: iri(row.decidedBy),
637
+ keyVersionId: iri(row.ofKeyVersion),
638
+ });
639
+ })
640
+ .sort((a, b) => {
641
+ var _a, _b;
642
+ return ((_a = b.createdAt) !== null && _a !== void 0 ? _a : '').localeCompare((_b = a.createdAt) !== null && _b !== void 0 ? _b : '') ||
643
+ b.id.localeCompare(a.id);
644
+ });
645
+ }
646
+ export async function getTranslationRevision(data) {
647
+ var _a;
648
+ const appId = requireText(data.appId, 'appId');
649
+ const revisionId = requireText(data.revisionId, 'revisionId');
650
+ const row = await TranslationRevision.select((revision) => [
651
+ revision.language,
652
+ revision.text,
653
+ revision.status,
654
+ revision.author,
655
+ revision.authorKind,
656
+ revision.mtProvider,
657
+ revision.basedOnText,
658
+ revision.note,
659
+ revision.createdAt,
660
+ revision.decidedAt,
661
+ revision.decidedBy,
662
+ revision.ofKeyVersion,
663
+ revision.ofKey.select((key) => [key.key]),
664
+ ])
665
+ .where((revision) => revision
666
+ .equals({ id: revisionId })
667
+ .and(revision.ofApplication.equals({ id: appId })))
668
+ .one()
669
+ .catch(() => null);
670
+ return row ? (_a = toRevisionRecords([row])[0]) !== null && _a !== void 0 ? _a : null : null;
671
+ }
672
+ /** Pending human proposals for the app, optionally narrowed to one language. */
673
+ export async function listTranslationProposals(data) {
674
+ var _a;
675
+ const appId = requireText(data.appId, 'appId');
676
+ const language = (_a = data.language) === null || _a === void 0 ? void 0 : _a.trim();
677
+ const rows = await TranslationRevision.select((revision) => [
678
+ revision.language,
679
+ revision.text,
680
+ revision.status,
681
+ revision.author,
682
+ revision.authorKind,
683
+ revision.basedOnText,
684
+ revision.note,
685
+ revision.createdAt,
686
+ revision.ofKeyVersion,
687
+ revision.ofKey.select((key) => [key.key]),
688
+ ]).where((revision) => {
689
+ const base = revision.ofApplication
690
+ .equals({ id: appId })
691
+ .and(revision.status.equals('proposed'));
692
+ return language ? base.and(revision.language.equals(language)) : base;
693
+ });
694
+ const proposals = toRevisionRecords(rows);
695
+ const entries = await listTranslationEntries({ appId });
696
+ const current = new Map(entries.flatMap((entry) => Object.entries(entry.units).map(([unitLanguage, unit]) => [
697
+ `${entry.key}\u0000${unitLanguage}`,
698
+ unit.text,
699
+ ])));
700
+ return proposals.map((proposal) => {
701
+ var _a, _b;
702
+ const currentText = (_a = current.get(`${proposal.key}\u0000${proposal.language}`)) !== null && _a !== void 0 ? _a : '';
703
+ return {
704
+ ...proposal,
705
+ currentText,
706
+ conflicted: ((_b = proposal.basedOnText) !== null && _b !== void 0 ? _b : '') !== currentText,
707
+ };
708
+ });
709
+ }
710
+ export async function decideTranslationProposal(data, reviewer) {
711
+ var _a, _b, _c, _d, _e, _f;
712
+ const proposal = await getTranslationRevision({
713
+ appId: data.appId,
714
+ revisionId: data.revisionId,
715
+ });
716
+ if (!proposal || proposal.status !== 'proposed') {
717
+ throw new Error('This translation proposal is no longer pending.');
718
+ }
719
+ const decisionNote = (_a = data.note) === null || _a === void 0 ? void 0 : _a.trim();
720
+ if (data.decision === 'reject' && !decisionNote) {
721
+ throw new Error('A reviewer note is required when rejecting a proposal.');
722
+ }
723
+ const now = new Date().toISOString();
724
+ let unit;
725
+ if (data.decision === 'accept') {
726
+ const currentEntries = await listTranslationEntries({ appId: data.appId });
727
+ const currentText = (_d = (_c = (_b = currentEntries.find(({ key }) => key === proposal.key)) === null || _b === void 0 ? void 0 : _b.units[proposal.language]) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : '';
728
+ if (((_e = proposal.basedOnText) !== null && _e !== void 0 ? _e : '') !== currentText) {
729
+ throw new Error('This proposal conflicts with a newer translation. Review it again before accepting.');
730
+ }
731
+ await upsertTranslationUnit({
732
+ appId: data.appId,
733
+ key: proposal.key,
734
+ language: proposal.language,
735
+ text: proposal.text,
736
+ state: 'reviewed',
737
+ }, {
738
+ author: reviewer,
739
+ authorKind: 'human',
740
+ note: data.note || `Accepted proposal ${proposal.id}.`,
741
+ });
742
+ unit = {
743
+ language: proposal.language,
744
+ text: proposal.text,
745
+ state: 'reviewed',
746
+ updatedAt: now,
747
+ };
748
+ const competing = await listTranslationProposals({
749
+ appId: data.appId,
750
+ language: proposal.language,
751
+ });
752
+ await Promise.all(competing
753
+ .filter(({ id, key }) => id !== proposal.id && key === proposal.key)
754
+ .map((revision) => TranslationRevision.update({
755
+ status: 'superseded',
756
+ note: `Superseded by accepted proposal ${proposal.id}.`,
757
+ decidedAt: new Date(now),
758
+ decidedBy: reviewer ? { id: reviewer } : undefined,
759
+ }).for({ id: revision.id })));
760
+ }
761
+ await TranslationRevision.update({
762
+ status: data.decision === 'accept' ? 'accepted' : 'rejected',
763
+ note: decisionNote || proposal.note,
764
+ decidedAt: new Date(now),
765
+ decidedBy: reviewer ? { id: reviewer } : undefined,
766
+ }).for({ id: proposal.id });
767
+ return {
768
+ revision: {
769
+ ...proposal,
770
+ status: data.decision === 'accept' ? 'accepted' : 'rejected',
771
+ note: decisionNote || proposal.note,
772
+ decidedAt: now,
773
+ decidedBy: reviewer,
774
+ conflicted: false,
775
+ currentText: (_f = unit === null || unit === void 0 ? void 0 : unit.text) !== null && _f !== void 0 ? _f : proposal.currentText,
776
+ },
777
+ unit,
778
+ };
779
+ }
780
+ /**
781
+ * Idempotent key write shared by the RPC provider and managed key-sync flows.
782
+ * Source drift marks every existing language unit stale before returning.
783
+ */
784
+ export async function upsertTranslationKey(data, authorship = {}) {
785
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
786
+ const appId = requireText(data.appId, 'appId');
787
+ const key = requireText(data.key, 'key');
788
+ requireText(data.sourceText, 'sourceText');
789
+ const id = keyIri(appId, key);
790
+ const values = {
791
+ namespace: (_a = data.namespace) !== null && _a !== void 0 ? _a : key.split('.').slice(0, -1).join('.'),
792
+ key,
793
+ sourceText: data.sourceText,
794
+ description: data.description,
795
+ kind: (_b = data.kind) !== null && _b !== void 0 ? _b : 'ui',
796
+ ofApplication: { id: appId },
797
+ ofNode: data.ofNode ? { id: data.ofNode } : undefined,
798
+ ofField: data.ofField,
799
+ ofShape: data.ofShape ? { id: data.ofShape } : undefined,
800
+ ofProperty: data.ofProperty ? { id: data.ofProperty } : undefined,
801
+ fromPackage: data.fromPackage,
802
+ overridden: data.overridden === true ? true : undefined,
803
+ format: (_c = data.format) !== null && _c !== void 0 ? _c : 'simple',
804
+ };
805
+ const existing = await TranslationKey.select((k) => [
806
+ k.sourceText,
807
+ k.format,
808
+ k.ofShape,
809
+ k.overridden,
810
+ k.currentVersion,
811
+ ])
812
+ .where((k) => k.equals({ id }))
813
+ .one()
814
+ .catch(() => null);
815
+ if (existing) {
816
+ const sourceChanged = existing.sourceText !== data.sourceText;
817
+ const wasOverridden = existing.overridden === true;
818
+ // AD-N override rule: editing the source of a SHAPE-owned key through the
819
+ // per-app path (this write) marks the app copy `overridden`, so shape-route
820
+ // propagation won't clobber the app's local wording. An explicit
821
+ // `data.overridden` (e.g. the installer clearing it) always wins.
822
+ const isShapeKey = !!existing.ofShape || !!data.ofShape;
823
+ if (data.overridden === undefined && isShapeKey && sourceChanged) {
824
+ values.overridden = true;
825
+ // Snapshot the canonical at FIRST divergence so "clear override" has a
826
+ // value to revert to even for a single-app project. Later edits keep the
827
+ // original snapshot (don't re-capture the already-overridden wording).
828
+ if (!wasOverridden)
829
+ values.shapeSource = existing.sourceText;
830
+ }
831
+ const currentVersion = await ensureCurrentKeyVersion({
832
+ appId,
833
+ keyId: id,
834
+ currentVersion: existing.currentVersion,
835
+ sourceLanguage: (_d = data.sourceLanguage) !== null && _d !== void 0 ? _d : 'en',
836
+ sourceText: (_e = existing.sourceText) !== null && _e !== void 0 ? _e : '',
837
+ format: existing.format === 'icu' ? 'icu' : 'simple',
838
+ createdBy: authorship.createdBy,
839
+ });
840
+ const decision = classifyKeyVersion(currentVersion, {
841
+ sourceText: data.sourceText,
842
+ format: (_f = data.format) !== null && _f !== void 0 ? _f : 'simple',
843
+ });
844
+ const nextVersion = decision.action === 'create-version'
845
+ ? await createTranslationKeyVersion({
846
+ appId,
847
+ keyId: id,
848
+ sourceLanguage: (_h = (_g = data.sourceLanguage) !== null && _g !== void 0 ? _g : currentVersion.sourceLanguage) !== null && _h !== void 0 ? _h : 'en',
849
+ sourceText: data.sourceText,
850
+ format: (_j = data.format) !== null && _j !== void 0 ? _j : 'simple',
851
+ supersedes: currentVersion.id,
852
+ createdBy: authorship.createdBy,
853
+ })
854
+ : currentVersion;
855
+ values.currentVersion = { id: nextVersion.id };
856
+ await TranslationKey.update(values).for({ id });
857
+ if (decision.action === 'create-version') {
858
+ await TranslationUnit.update({ state: 'stale' }).where((u) => u.ofKey.equals({ id }));
859
+ }
860
+ return {
861
+ id,
862
+ versionId: nextVersion.id,
863
+ versionCreated: decision.action === 'create-version',
864
+ };
865
+ }
866
+ await TranslationKey.create({ __id: id, ...values });
867
+ const firstVersion = await createTranslationKeyVersion({
868
+ appId,
869
+ keyId: id,
870
+ sourceLanguage: (_k = data.sourceLanguage) !== null && _k !== void 0 ? _k : 'en',
871
+ sourceText: data.sourceText,
872
+ format: (_l = data.format) !== null && _l !== void 0 ? _l : 'simple',
873
+ createdBy: authorship.createdBy,
874
+ });
875
+ await TranslationKey.update({
876
+ currentVersion: { id: firstVersion.id },
877
+ }).for({ id });
878
+ return { id, versionId: firstVersion.id, versionCreated: true };
879
+ }
880
+ /**
881
+ * Append one revision WITHOUT touching the unit (Plan 017 AD-P) — the write
882
+ * path for MT SUGGESTIONS (Slice B) and translator PROPOSALS (Slice C): a
883
+ * pending value a reviewer/editor applies later. `basedOnText` should carry
884
+ * the unit text visible to the author, for conflict detection at decision time.
885
+ */
886
+ export async function createTranslationRevision(data) {
887
+ var _a;
888
+ const appId = requireText(data.appId, 'appId');
889
+ const key = requireText(data.key, 'key');
890
+ const language = requireText(data.language, 'language');
891
+ const { keyId, version } = await requireKeyWriteContext({
892
+ appId,
893
+ key,
894
+ createdBy: data.author,
895
+ });
896
+ const id = revisionIri(version.id, language);
897
+ await TranslationRevision.create({
898
+ __id: id,
899
+ ofApplication: { id: appId },
900
+ ofKey: { id: keyId },
901
+ ofKeyVersion: { id: version.id },
902
+ language,
903
+ text: data.text,
904
+ status: data.status,
905
+ author: data.author ? { id: data.author } : undefined,
906
+ authorKind: (_a = data.authorKind) !== null && _a !== void 0 ? _a : 'human',
907
+ mtProvider: data.mtProvider,
908
+ basedOnText: data.basedOnText,
909
+ note: data.note,
910
+ createdAt: new Date(),
911
+ });
912
+ return { id };
913
+ }
914
+ /**
915
+ * Idempotent unit write shared by the RPC provider, import, and (P4) MT flows.
916
+ * Every write that CHANGES the text also records an append-only
917
+ * `status:'applied'` `TranslationRevision` (Plan 017 AD-P) carrying the
918
+ * previous text as `basedOnText` — the audit trail behind the history UI.
919
+ * Re-saving identical text updates state/provenance without a revision.
920
+ */
921
+ export async function upsertTranslationUnit(data, authorship = {}) {
922
+ var _a, _b, _c, _d;
923
+ const appId = requireText(data.appId, 'appId');
924
+ const key = requireText(data.key, 'key');
925
+ const language = requireText(data.language, 'language');
926
+ const author = (_a = authorship.author) !== null && _a !== void 0 ? _a : data.updatedBy;
927
+ const { keyId, version } = await requireKeyWriteContext({
928
+ appId,
929
+ key,
930
+ createdBy: author,
931
+ });
932
+ const now = new Date().toISOString();
933
+ const values = {
934
+ ofApplication: { id: appId },
935
+ ofKey: { id: keyId },
936
+ ofKeyVersion: { id: version.id },
937
+ language,
938
+ text: data.text,
939
+ state: (_b = data.state) !== null && _b !== void 0 ? _b : 'reviewed',
940
+ updatedAt: new Date(now),
941
+ updatedBy: author ? { id: author } : undefined,
942
+ origin: authorship.mtProvider === 'tm' ? 'tm' : '',
943
+ };
944
+ const existing = await TranslationUnit.select((u) => [u.text])
945
+ .where((u) => u.language
946
+ .equals(language)
947
+ .and(u.ofKeyVersion.equals({ id: version.id })))
948
+ .one()
949
+ .catch(() => null);
950
+ const id = (_c = iri(existing === null || existing === void 0 ? void 0 : existing.id)) !== null && _c !== void 0 ? _c : unitIri(version.id, language);
951
+ if (existing)
952
+ await TranslationUnit.update(values).for({ id });
953
+ else
954
+ await TranslationUnit.create({ __id: id, ...values });
955
+ const previousText = existing === null || existing === void 0 ? void 0 : existing.text;
956
+ if (existing && previousText === data.text)
957
+ return { id };
958
+ const revisionId = revisionIri(version.id, language);
959
+ await TranslationRevision.create({
960
+ __id: revisionId,
961
+ ofApplication: { id: appId },
962
+ ofKey: { id: keyId },
963
+ ofKeyVersion: { id: version.id },
964
+ language,
965
+ text: data.text,
966
+ status: 'applied',
967
+ author: author ? { id: author } : undefined,
968
+ authorKind: (_d = authorship.authorKind) !== null && _d !== void 0 ? _d : 'human',
969
+ mtProvider: authorship.mtProvider,
970
+ basedOnText: previousText,
971
+ note: authorship.note,
972
+ createdAt: new Date(now),
973
+ });
974
+ return { id, revisionId };
975
+ }
976
+ /** One app-scoped lookup powers both exact TM and same-key carry-forward. */
977
+ export async function listTranslationMemoryMatches(data) {
978
+ const appId = requireText(data.appId, 'appId');
979
+ const key = requireText(data.key, 'key');
980
+ const language = requireText(data.language, 'language');
981
+ const keyId = keyIri(appId, key);
982
+ const keyRow = await TranslationKey.select((candidate) => [
983
+ candidate.currentVersion,
984
+ ])
985
+ .where((candidate) => candidate.equals({ id: keyId }))
986
+ .one()
987
+ .catch(() => null);
988
+ const currentVersionId = iri(keyRow === null || keyRow === void 0 ? void 0 : keyRow.currentVersion);
989
+ const currentVersion = currentVersionId
990
+ ? await readKeyVersion(currentVersionId)
991
+ : null;
992
+ if (!currentVersion)
993
+ return [];
994
+ const rows = await TranslationUnit.select((unit) => [
995
+ unit.language,
996
+ unit.text,
997
+ unit.state,
998
+ unit.updatedAt,
999
+ unit.ofKey.select((candidate) => [candidate.key]),
1000
+ unit.ofKeyVersion.select((version) => [
1001
+ version.sourceText,
1002
+ version.sourceHash,
1003
+ version.contractHash,
1004
+ ]),
1005
+ ]).where((unit) => unit.ofApplication
1006
+ .equals({ id: appId })
1007
+ .and(unit.language.equals(language))
1008
+ .and(unit.state.equals('reviewed')));
1009
+ return classifyTranslationMemoryMatches({
1010
+ key,
1011
+ language,
1012
+ currentVersion,
1013
+ rows: rows,
1014
+ });
1015
+ }
1016
+ /** App-scoped reviewed-unit index for search, audit, and exact reuse. */
1017
+ export async function listTranslationMemory(data) {
1018
+ const appId = requireText(data.appId, 'appId');
1019
+ const rows = await TranslationUnit.select((unit) => [
1020
+ unit.language,
1021
+ unit.text,
1022
+ unit.state,
1023
+ unit.updatedAt,
1024
+ unit.ofKey.select((candidate) => [candidate.key]),
1025
+ unit.ofKeyVersion.select((version) => [
1026
+ version.sourceText,
1027
+ version.sourceHash,
1028
+ version.contractHash,
1029
+ ]),
1030
+ ]).where((unit) => unit.ofApplication
1031
+ .equals({ id: appId })
1032
+ .and(unit.state.equals('reviewed')));
1033
+ return toTranslationMemoryRecords(rows);
1034
+ }
1035
+ /** Preview or apply every safe exact-match TM candidate for one language. */
1036
+ export async function pretranslateFromMemory(data, authorship = {}) {
1037
+ const appId = requireText(data.appId, 'appId');
1038
+ const language = requireText(data.language, 'language');
1039
+ const [entries, memory] = await Promise.all([
1040
+ listTranslationEntries({ appId }),
1041
+ listTranslationMemory({ appId }),
1042
+ ]);
1043
+ const candidates = findExactMemoryPretranslations({
1044
+ entries,
1045
+ memory,
1046
+ language,
1047
+ });
1048
+ if (!data.dryRun) {
1049
+ for (const candidate of candidates) {
1050
+ await upsertTranslationUnit({
1051
+ appId,
1052
+ key: candidate.key,
1053
+ language,
1054
+ text: candidate.text,
1055
+ state: 'machine',
1056
+ }, {
1057
+ author: authorship.author,
1058
+ authorKind: 'machine',
1059
+ mtProvider: 'tm',
1060
+ note: `Exact translation-memory match from ${candidate.sourceKey}.`,
1061
+ });
1062
+ }
1063
+ }
1064
+ return {
1065
+ language,
1066
+ eligible: candidates.length,
1067
+ translated: data.dryRun ? 0 : candidates.length,
1068
+ candidates,
1069
+ };
1070
+ }
1071
+ /** App-data termbase read shared by Studio, MT, and publish quality gates. */
1072
+ export async function listGlossaryTerms(data) {
1073
+ requireText(data.appId, 'appId');
1074
+ const rows = await GlossaryTerm.select((glossary) => [
1075
+ glossary.term,
1076
+ glossary.translation,
1077
+ glossary.language,
1078
+ glossary.description,
1079
+ glossary.termType,
1080
+ glossary.useInstead,
1081
+ glossary.caseSensitive,
1082
+ ]).catch(() => []);
1083
+ return (rows !== null && rows !== void 0 ? rows : [])
1084
+ .filter((row) => row === null || row === void 0 ? void 0 : row.term)
1085
+ .map((row) => {
1086
+ var _a, _b;
1087
+ return ({
1088
+ id: typeof row.id === 'string' ? row.id : ((_b = (_a = row.id) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : ''),
1089
+ term: row.term,
1090
+ termType: row.termType === 'keep' || row.termType === 'forbid' || row.termType === 'require'
1091
+ ? row.termType
1092
+ : 'prefer',
1093
+ translation: row.translation || undefined,
1094
+ useInstead: row.useInstead || undefined,
1095
+ language: row.language || undefined,
1096
+ caseSensitive: row.caseSensitive === true || row.caseSensitive === 'true'
1097
+ ? true
1098
+ : undefined,
1099
+ description: row.description || undefined,
1100
+ });
1101
+ })
1102
+ .sort((left, right) => {
1103
+ var _a, _b;
1104
+ return left.term.localeCompare(right.term) ||
1105
+ ((_a = left.language) !== null && _a !== void 0 ? _a : '').localeCompare((_b = right.language) !== null && _b !== void 0 ? _b : '');
1106
+ });
1107
+ }
1108
+ /**
1109
+ * Apply a server-validated memory candidate. Exact matches retain `machine`
1110
+ * state and `tm` provenance; changed-source carry-forward is an explicit
1111
+ * reviewer action and becomes reviewed.
1112
+ */
1113
+ export async function applyTranslationMemoryMatch(data, authorship = {}) {
1114
+ const unitId = requireText(data.unitId, 'unitId');
1115
+ const match = (await listTranslationMemoryMatches({
1116
+ appId: data.appId,
1117
+ key: data.key,
1118
+ language: data.language,
1119
+ })).find((candidate) => candidate.unitId === unitId);
1120
+ if (!match) {
1121
+ throw new Error('Translation-memory candidate is no longer compatible with this key version.');
1122
+ }
1123
+ const state = match.kind === 'exact' ? 'machine' : 'reviewed';
1124
+ await upsertTranslationUnit({
1125
+ appId: data.appId,
1126
+ key: data.key,
1127
+ language: data.language,
1128
+ text: match.text,
1129
+ state,
1130
+ }, {
1131
+ author: authorship.author,
1132
+ authorKind: match.kind === 'exact' ? 'machine' : 'human',
1133
+ mtProvider: match.kind === 'exact' ? 'tm' : undefined,
1134
+ note: match.kind === 'exact'
1135
+ ? `Exact translation-memory match from ${match.key}.`
1136
+ : `Reviewed carry-forward from ${match.keyVersionId}.`,
1137
+ });
1138
+ return {
1139
+ language: data.language,
1140
+ text: match.text,
1141
+ state,
1142
+ origin: match.kind === 'exact' ? 'tm' : undefined,
1143
+ updatedAt: new Date().toISOString(),
1144
+ };
1145
+ }
1146
+ /** Server boundary for app-scoped translation keys and language units. */
1147
+ export class TranslationProvider extends ShapeProvider {
1148
+ constructor() {
1149
+ super(...arguments);
1150
+ this.shape = TranslationKey;
1151
+ }
1152
+ /**
1153
+ * Authoring gate. Reads `linkedAuth` off the request the LINKED server already
1154
+ * populates — framework-level, so this stays portable (no CN/create-now-js
1155
+ * import, AD-G). Anonymous callers can no longer read or write an app's
1156
+ * translation content via `/call`. Fine-grained "is translation enabled for
1157
+ * this app + is the caller a project member" gating is a Create Now concern and
1158
+ * is enforced additionally at the CN route layer (`isTranslationEnabledForApp`).
1159
+ * `getMessages` intentionally stays public — it is the app's runtime string
1160
+ * fetch, not an authoring surface.
1161
+ */
1162
+ requireAuthor() {
1163
+ var _a, _b;
1164
+ const account = (_b = (_a = this.request) === null || _a === void 0 ? void 0 : _a.linkedAuth) === null || _b === void 0 ? void 0 : _b.userAccount;
1165
+ if (!account) {
1166
+ throw new Error('Authentication required to read or edit translations.');
1167
+ }
1168
+ }
1169
+ async requireAccess(data, action) {
1170
+ this.requireAuthor();
1171
+ const actorWebId = this.authorIri();
1172
+ if (!actorWebId ||
1173
+ !(await canAuthorTranslation({
1174
+ appId: data.appId,
1175
+ actorWebId,
1176
+ action,
1177
+ language: data.language,
1178
+ }))) {
1179
+ throw new Error(`Translation ${action} permission required.`);
1180
+ }
1181
+ }
1182
+ async listKeys(data) {
1183
+ this.requireAuthor();
1184
+ return listTranslationKeys(data);
1185
+ }
1186
+ async listEntries(data) {
1187
+ this.requireAuthor();
1188
+ return listTranslationEntries(data);
1189
+ }
1190
+ async listKeyVersions(data) {
1191
+ this.requireAuthor();
1192
+ return listTranslationKeyVersions(data);
1193
+ }
1194
+ async listMemoryMatches(data) {
1195
+ this.requireAuthor();
1196
+ return listTranslationMemoryMatches(data);
1197
+ }
1198
+ async proposeRevision(data) {
1199
+ var _a, _b, _c;
1200
+ await this.requireAccess(data, 'propose');
1201
+ const currentEntries = await listTranslationEntries({ appId: data.appId });
1202
+ const basedOnText = (_c = (_b = (_a = currentEntries.find(({ key }) => key === data.key)) === null || _a === void 0 ? void 0 : _a.units[data.language]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : '';
1203
+ return createTranslationRevision({
1204
+ ...data,
1205
+ basedOnText,
1206
+ status: 'proposed',
1207
+ author: this.authorIri(),
1208
+ authorKind: 'human',
1209
+ });
1210
+ }
1211
+ async listProposals(data) {
1212
+ this.requireAuthor();
1213
+ const actorWebId = this.authorIri();
1214
+ if (!actorWebId)
1215
+ throw new Error('Translation read permission required.');
1216
+ const proposals = await listTranslationProposals(data);
1217
+ const allowed = await Promise.all(proposals.map((proposal) => canAuthorTranslation({
1218
+ appId: data.appId,
1219
+ actorWebId,
1220
+ action: 'read',
1221
+ language: proposal.language,
1222
+ })));
1223
+ return proposals.filter((_, index) => allowed[index]);
1224
+ }
1225
+ async decideProposal(data) {
1226
+ const proposal = await getTranslationRevision({
1227
+ appId: data.appId,
1228
+ revisionId: data.revisionId,
1229
+ });
1230
+ if (!proposal) {
1231
+ throw new Error('This translation proposal is no longer pending.');
1232
+ }
1233
+ await this.requireAccess({ appId: data.appId, language: proposal.language }, 'review');
1234
+ return decideTranslationProposal(data, this.authorIri());
1235
+ }
1236
+ async listMemory(data) {
1237
+ this.requireAuthor();
1238
+ return listTranslationMemory(data);
1239
+ }
1240
+ async pretranslateMemory(data) {
1241
+ await this.requireAccess(data, 'run-mt');
1242
+ return pretranslateFromMemory(data, { author: this.authorIri() });
1243
+ }
1244
+ async applyMemoryMatch(data) {
1245
+ await this.requireAccess(data, 'review');
1246
+ return applyTranslationMemoryMatch(data, { author: this.authorIri() });
1247
+ }
1248
+ async listReleases(data) {
1249
+ this.requireAuthor();
1250
+ return listTranslationReleases(data);
1251
+ }
1252
+ async createRelease(data) {
1253
+ this.requireAuthor();
1254
+ return createTranslationRelease(data);
1255
+ }
1256
+ async advanceReleaseHotfix(data) {
1257
+ this.requireAuthor();
1258
+ return advanceTranslationReleaseHotfix(data);
1259
+ }
1260
+ async upsertKey(data) {
1261
+ this.requireAuthor();
1262
+ return upsertTranslationKey(data, { createdBy: this.authorIri() });
1263
+ }
1264
+ /** WebID of the signed-in caller — never trust a client-supplied author. */
1265
+ authorIri() {
1266
+ var _a, _b, _c, _d, _e, _f, _g;
1267
+ const auth = (_a = this.request) === null || _a === void 0 ? void 0 : _a.linkedAuth;
1268
+ const webId = (_g = (_e = (_d = (_c = (_b = auth === null || auth === void 0 ? void 0 : auth.userAccount) === null || _b === void 0 ? void 0 : _b.accountOf) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : auth === null || auth === void 0 ? void 0 : auth.webId) !== null && _e !== void 0 ? _e : (_f = auth === null || auth === void 0 ? void 0 : auth.userAccount) === null || _f === void 0 ? void 0 : _f.id) !== null && _g !== void 0 ? _g : auth === null || auth === void 0 ? void 0 : auth.userAccount;
1269
+ return typeof webId === 'string' && webId ? webId : undefined;
1270
+ }
1271
+ async upsertUnit(data) {
1272
+ await this.requireAccess(data, 'review');
1273
+ return upsertTranslationUnit({ ...data, updatedBy: undefined }, { author: this.authorIri(), authorKind: 'human' });
1274
+ }
1275
+ /**
1276
+ * Glossary CRUD (Plan 017 Slice B). App-data is branch-routed per app, so
1277
+ * every GlossaryTerm in context belongs to the calling app; the appId only
1278
+ * anchors the IRIs. Uniqueness = (term, language) — upsert is
1279
+ * delete-then-create on that identity.
1280
+ */
1281
+ async listGlossary(data) {
1282
+ this.requireAuthor();
1283
+ return listGlossaryTerms(data);
1284
+ }
1285
+ async upsertGlossaryTerm(data) {
1286
+ var _a, _b, _c, _d, _e;
1287
+ await this.requireAccess(data, 'manage');
1288
+ const appId = requireText(data.appId, 'appId');
1289
+ const term = requireText(data.term, 'term');
1290
+ const language = ((_a = data.language) === null || _a === void 0 ? void 0 : _a.trim()) || '';
1291
+ const termType = data.termType === 'keep' || data.termType === 'forbid' || data.termType === 'require'
1292
+ ? data.termType
1293
+ : 'prefer';
1294
+ if (termType === 'require' && !((_b = data.translation) === null || _b === void 0 ? void 0 : _b.trim())) {
1295
+ throw new Error('A required glossary term needs a translation.');
1296
+ }
1297
+ const id = `${appId.replace(/\/$/, '')}/translation/glossary/${encodeURIComponent(term)}--${encodeURIComponent(language || 'all')}`;
1298
+ // Delete-then-create: INSERT DATA is add-only, re-saving would duplicate.
1299
+ await GlossaryTerm.delete({ id }).catch(() => { });
1300
+ await GlossaryTerm.create({
1301
+ __id: id,
1302
+ term,
1303
+ termType,
1304
+ translation: termType === 'prefer' || termType === 'require' ? ((_c = data.translation) === null || _c === void 0 ? void 0 : _c.trim()) || undefined : undefined,
1305
+ useInstead: termType === 'forbid' ? ((_d = data.useInstead) === null || _d === void 0 ? void 0 : _d.trim()) || undefined : undefined,
1306
+ language: language || undefined,
1307
+ caseSensitive: data.caseSensitive === true ? true : undefined,
1308
+ description: ((_e = data.description) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
1309
+ });
1310
+ return { id };
1311
+ }
1312
+ async deleteGlossaryTerm(data) {
1313
+ await this.requireAccess(data, 'manage');
1314
+ const appId = requireText(data.appId, 'appId');
1315
+ const id = requireText(data.id, 'id');
1316
+ // Only glossary nodes under this app's namespace may be deleted here.
1317
+ if (!id.startsWith(`${appId.replace(/\/$/, '')}/translation/glossary/`)) {
1318
+ throw new Error('Not a glossary entry of this app.');
1319
+ }
1320
+ await GlossaryTerm.delete({ id });
1321
+ return { deleted: true };
1322
+ }
1323
+ /** History for one (key, language) cell — newest first (Plan 017 AD-P). */
1324
+ async listRevisions(data) {
1325
+ this.requireAuthor();
1326
+ const appId = requireText(data.appId, 'appId');
1327
+ const key = requireText(data.key, 'key');
1328
+ const language = requireText(data.language, 'language');
1329
+ const keyId = keyIri(appId, key);
1330
+ const rows = await TranslationRevision.select((r) => [
1331
+ r.language,
1332
+ r.text,
1333
+ r.status,
1334
+ r.author,
1335
+ r.authorKind,
1336
+ r.mtProvider,
1337
+ r.basedOnText,
1338
+ r.note,
1339
+ r.createdAt,
1340
+ r.decidedAt,
1341
+ r.decidedBy,
1342
+ r.ofKeyVersion,
1343
+ r.ofKey.select((k) => [k.key]),
1344
+ ]).where((r) => r.language.equals(language).and(r.ofKey.equals({ id: keyId })));
1345
+ return toRevisionRecords(rows);
1346
+ }
1347
+ async getMessages(data) {
1348
+ const appId = requireText(data.appId, 'appId');
1349
+ const language = requireText(data.language, 'language');
1350
+ const rows = await TranslationUnit.select((u) => [
1351
+ u.text,
1352
+ u.ofKeyVersion,
1353
+ u.ofKey.select((k) => [k.key, k.format, k.currentVersion]),
1354
+ ]).where((u) => u.language
1355
+ .equals(language)
1356
+ .and(u.ofKey.ofApplication.equals({ id: appId })));
1357
+ return Object.fromEntries((rows !== null && rows !== void 0 ? rows : [])
1358
+ .filter((row) => {
1359
+ var _a;
1360
+ const currentVersion = iri((_a = row.ofKey) === null || _a === void 0 ? void 0 : _a.currentVersion);
1361
+ const unitVersion = iri(row.ofKeyVersion);
1362
+ return !currentVersion || currentVersion === unitVersion;
1363
+ })
1364
+ .map((row) => {
1365
+ var _a, _b;
1366
+ return [
1367
+ (_a = row.ofKey) === null || _a === void 0 ? void 0 : _a.key,
1368
+ ((_b = row.ofKey) === null || _b === void 0 ? void 0 : _b.format) === 'icu'
1369
+ ? { message: row.text, format: 'icu' }
1370
+ : row.text,
1371
+ ];
1372
+ })
1373
+ .filter(([key]) => typeof key === 'string' && key.length > 0));
1374
+ }
1375
+ }
1376
+ //# sourceMappingURL=TranslationProvider.js.map