@_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,33 @@
1
+ export type TranslationAuthoringAction =
2
+ | 'manage'
3
+ | 'review'
4
+ | 'propose'
5
+ | 'run-mt'
6
+ | 'read';
7
+
8
+ export interface TranslationAuthorizationRequest {
9
+ appId: string;
10
+ actorWebId: string;
11
+ action: TranslationAuthoringAction;
12
+ language?: string;
13
+ }
14
+
15
+ export type TranslationAuthorizationResolver = (
16
+ request: TranslationAuthorizationRequest,
17
+ ) => Promise<boolean>;
18
+
19
+ let resolver: TranslationAuthorizationResolver | undefined;
20
+
21
+ /** Host hook: Create Now supplies CN-control-plane assignment evaluation. */
22
+ export function configureTranslationAuthorization(
23
+ next?: TranslationAuthorizationResolver,
24
+ ): void {
25
+ resolver = next;
26
+ }
27
+
28
+ /** Ejected apps may install @_linked/access; authenticated legacy hosts allow. */
29
+ export async function canAuthorTranslation(
30
+ request: TranslationAuthorizationRequest,
31
+ ): Promise<boolean> {
32
+ return resolver ? resolver(request) : true;
33
+ }
package/src/backend.ts ADDED
@@ -0,0 +1,2 @@
1
+ // The backend loader instantiates every named export; export Providers only.
2
+ export { TranslationProvider } from './shapes/TranslationProvider.js';
@@ -0,0 +1,264 @@
1
+ import {
2
+ defineLanguage,
3
+ validateLanguageDefinitions,
4
+ type TranslationLanguageDefinition,
5
+ } from './languages.js';
6
+ /**
7
+ * `loadMessages` factory for the CDN delivery cascade (Plan 016 P2.4, AD-J/AD-K).
8
+ * SIMPLE-POINTER mode (the ratified default): fetch `${base}/{lang}.json` from
9
+ * the published CDN — changing `base` (an app's `tr:cdnTarget`) repoints at
10
+ * different cloud files with no call-site change, exactly the Tolgee model.
11
+ * `bundledPath` (same-origin bundled copy) and `devApi` (e.g. the `getMessages`
12
+ * RPC for live dev editing) are optional fallbacks, tried in the order given.
13
+ * Any source that yields a non-empty map wins; all-empty ⇒ `{}` and the client
14
+ * keeps its inline English defaults. Never throws.
15
+ *
16
+ * Portable (no CN import): the caller wires `base`/`devApi` for its environment.
17
+ */
18
+ import type { TranslationMessages } from './core/messages.js';
19
+ import { sha256Hex } from './key-version.js';
20
+ import {
21
+ applyCatalogPatches,
22
+ type TranslationBuildDescriptor,
23
+ type TranslationCatalogPatch,
24
+ type TranslationReleaseManifest,
25
+ } from './release.js';
26
+
27
+ export interface CdnLoaderConfig {
28
+ /** Published CDN base — `${base}/{lang}.json`. The repoint knob. Omit to skip. */
29
+ base?: string;
30
+ /** Same-origin bundled base — `${bundledPath}/{lang}.json`. Omit to skip. */
31
+ bundledPath?: string;
32
+ /** Last-resort loader (e.g. a live backend RPC). Omit to skip. */
33
+ devApi?: (language: string) => Promise<TranslationMessages>;
34
+ /** Injectable for tests / non-DOM runtimes. */
35
+ fetchImpl?: typeof fetch;
36
+ }
37
+
38
+ function isNonEmptyMap(v: unknown): v is TranslationMessages {
39
+ return !!v && typeof v === 'object' && Object.keys(v as object).length > 0;
40
+ }
41
+
42
+ export function createCdnLoader(
43
+ config: CdnLoaderConfig
44
+ ): (language: string) => Promise<TranslationMessages> {
45
+ const doFetch =
46
+ config.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
47
+ const base = config.base?.replace(/\/+$/, '');
48
+ const bundled = config.bundledPath?.replace(/\/+$/, '');
49
+
50
+ const fetchJson = async (
51
+ url: string
52
+ ): Promise<TranslationMessages | null> => {
53
+ if (!doFetch) return null;
54
+ try {
55
+ const res = await doFetch(url);
56
+ if (!res.ok) return null;
57
+ return (await res.json()) as TranslationMessages;
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+
63
+ return async (language: string): Promise<TranslationMessages> => {
64
+ const sources: Array<() => Promise<TranslationMessages | null>> = [];
65
+ if (base) sources.push(() => fetchJson(`${base}/${language}.json`));
66
+ if (bundled) sources.push(() => fetchJson(`${bundled}/${language}.json`));
67
+ if (config.devApi)
68
+ sources.push(async () => {
69
+ try {
70
+ return await config.devApi!(language);
71
+ } catch {
72
+ return null;
73
+ }
74
+ });
75
+
76
+ for (const source of sources) {
77
+ const result = await source();
78
+ if (isNonEmptyMap(result)) return result;
79
+ }
80
+ return {};
81
+ };
82
+ }
83
+
84
+ export interface ReleaseLoaderConfig {
85
+ bundledPath: string;
86
+ descriptor:
87
+ | TranslationBuildDescriptor
88
+ | (() => Promise<TranslationBuildDescriptor | null>);
89
+ fetchImpl?: typeof fetch;
90
+ devApi?: (language: string) => Promise<TranslationMessages>;
91
+ }
92
+
93
+ function resolveReleaseUrl(url: string, manifestUrl: string): string {
94
+ try {
95
+ return new URL(url, manifestUrl).toString();
96
+ } catch {
97
+ return url;
98
+ }
99
+ }
100
+
101
+ function isReleaseManifest(
102
+ value: unknown
103
+ ): value is TranslationReleaseManifest {
104
+ const manifest = value as Partial<TranslationReleaseManifest> | undefined;
105
+ return (
106
+ manifest?.schemaVersion === 2 &&
107
+ typeof manifest.releaseId === 'string' &&
108
+ typeof manifest.contractSetHash === 'string' &&
109
+ !!manifest.keyVersions &&
110
+ typeof manifest.keyVersions === 'object' &&
111
+ !!manifest.languages &&
112
+ typeof manifest.languages === 'object'
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Schema-v2 release loader. Bundled messages always win the first render;
118
+ * compatible immutable CDN objects fetched in the background become visible
119
+ * only on the next call for that language.
120
+ */
121
+ export function createReleaseLoader(
122
+ config: ReleaseLoaderConfig
123
+ ): (language: string) => Promise<TranslationMessages> {
124
+ const doFetch =
125
+ config.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
126
+ const bundled = config.bundledPath.replace(/\/+$/, '');
127
+ const objectCache = new Map<string, unknown>();
128
+ const pending = new Map<string, Promise<TranslationMessages | null>>();
129
+ const requested = new Set<string>();
130
+ let descriptorPromise: Promise<TranslationBuildDescriptor | null> | undefined;
131
+ const getDescriptor = () =>
132
+ (descriptorPromise ??=
133
+ typeof config.descriptor === 'function'
134
+ ? config.descriptor()
135
+ : Promise.resolve(config.descriptor));
136
+
137
+ const fetchText = async (url: string): Promise<string | null> => {
138
+ if (!doFetch) return null;
139
+ try {
140
+ const response = await doFetch(url);
141
+ return response.ok ? await response.text() : null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ };
146
+
147
+ const fetchMessages = async (
148
+ url: string
149
+ ): Promise<TranslationMessages | null> => {
150
+ const text = await fetchText(url);
151
+ if (text === null) return null;
152
+ try {
153
+ const parsed = JSON.parse(text);
154
+ return isNonEmptyMap(parsed) ? parsed : null;
155
+ } catch {
156
+ return null;
157
+ }
158
+ };
159
+
160
+ const fetchHashed = async <T>(
161
+ url: string,
162
+ hash: string
163
+ ): Promise<T | null> => {
164
+ if (objectCache.has(hash)) return objectCache.get(hash) as T;
165
+ const text = await fetchText(url);
166
+ if (text === null || (await sha256Hex(text)) !== hash) return null;
167
+ try {
168
+ const parsed = JSON.parse(text) as T;
169
+ objectCache.set(hash, parsed);
170
+ return parsed;
171
+ } catch {
172
+ return null;
173
+ }
174
+ };
175
+
176
+ const fetchReleaseLanguage = async (
177
+ language: string
178
+ ): Promise<TranslationMessages | null> => {
179
+ const descriptor = await getDescriptor();
180
+ if (!descriptor) return null;
181
+ const manifestText = await fetchText(descriptor.manifestUrl);
182
+ if (manifestText === null) return null;
183
+ let manifest: unknown;
184
+ try {
185
+ manifest = JSON.parse(manifestText);
186
+ } catch {
187
+ return null;
188
+ }
189
+ if (
190
+ !isReleaseManifest(manifest) ||
191
+ manifest.releaseId !== descriptor.releaseId ||
192
+ manifest.contractSetHash !== descriptor.contractSetHash
193
+ ) {
194
+ return null;
195
+ }
196
+ const lane = manifest.languages[language];
197
+ if (!lane) return null;
198
+ const base = await fetchHashed<TranslationMessages>(
199
+ resolveReleaseUrl(lane.base.url, descriptor.manifestUrl),
200
+ lane.base.hash
201
+ );
202
+ if (!base) return null;
203
+ const patches: TranslationCatalogPatch[] = [];
204
+ for (const patch of [...lane.patches].sort(
205
+ (left, right) => left.sequence - right.sequence
206
+ )) {
207
+ const body = await fetchHashed<TranslationCatalogPatch>(
208
+ resolveReleaseUrl(patch.url, descriptor.manifestUrl),
209
+ patch.hash
210
+ );
211
+ if (!body) return null;
212
+ patches.push(body);
213
+ }
214
+ return applyCatalogPatches(base, patches);
215
+ };
216
+
217
+ return async (language: string): Promise<TranslationMessages> => {
218
+ const bundledMessages = await fetchMessages(`${bundled}/${language}.json`);
219
+ const existing = pending.get(language);
220
+ if (requested.has(language) && existing) {
221
+ const released = await existing;
222
+ return released ?? bundledMessages ?? {};
223
+ }
224
+ requested.add(language);
225
+ const background = fetchReleaseLanguage(language);
226
+ pending.set(language, background);
227
+ if (bundledMessages) {
228
+ void background.catch(() => null);
229
+ return bundledMessages;
230
+ }
231
+ const released = await background;
232
+ if (released) return released;
233
+ if (config.devApi) {
234
+ try {
235
+ return await config.devApi(language);
236
+ } catch {
237
+ // Preserve the empty fallback contract.
238
+ }
239
+ }
240
+ return {};
241
+ };
242
+ }
243
+
244
+ /** Fetch public language resources independently of a compiled application. */
245
+ export function createCdnLanguageLoader(
246
+ config: Pick<CdnLoaderConfig, 'base' | 'fetchImpl'>
247
+ ): () => Promise<TranslationLanguageDefinition[]> {
248
+ return async () => {
249
+ if (!config.base) throw new Error('A translation CDN base is required.');
250
+ const response = await (config.fetchImpl ?? fetch)(
251
+ `${config.base.replace(/\/+$/, '')}/languages.json`
252
+ );
253
+ if (!response.ok)
254
+ throw new Error(`Language catalog HTTP ${response.status}`);
255
+ const catalog = await response.json();
256
+ if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.languages))
257
+ throw new Error('Invalid language catalog.');
258
+ const definitions = catalog.languages.map(
259
+ (item: TranslationLanguageDefinition) => defineLanguage(item.code, item)
260
+ );
261
+ validateLanguageDefinitions(definitions);
262
+ return definitions;
263
+ };
264
+ }