@_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
@@ -1,56 +0,0 @@
1
- import { sharedState } from '@_linked/core/utils/sharedState';
2
-
3
- export type TranslationAuthoringAction =
4
- | 'manage'
5
- | 'review'
6
- | 'propose'
7
- | 'run-mt'
8
- | 'read';
9
-
10
- export interface TranslationAuthorizationRequest {
11
- appId: string;
12
- actorWebId: string;
13
- action: TranslationAuthoringAction;
14
- language?: string;
15
- }
16
-
17
- export type TranslationAuthorizationResolver = (
18
- request: TranslationAuthorizationRequest,
19
- ) => Promise<boolean>;
20
-
21
- // On globalThis so a second copy of this module sees the resolver the host configured on the
22
- // first; a copy that cannot see it would deny every author.
23
- const state = sharedState('@_linked/translation', 'authorization', () => ({
24
- resolver: undefined as TranslationAuthorizationResolver | undefined,
25
- }));
26
-
27
- /**
28
- * Host hook: the host decides who may read and author an app's translations
29
- * (for example by evaluating its own translator assignments and project roles).
30
- *
31
- * The resolver is the only source of a "yes". A host that does not configure
32
- * one gets no authoring or read access over the translation provider; only the
33
- * public runtime fetch (`getMessages`) keeps working.
34
- */
35
- export function configureTranslationAuthorization(
36
- next?: TranslationAuthorizationResolver,
37
- ): void {
38
- state.resolver = next;
39
- }
40
-
41
- /**
42
- * Whether the host's resolver grants `request`. Denies when no resolver is
43
- * configured, and when the resolver throws or answers anything but `true`.
44
- */
45
- export async function canAuthorTranslation(
46
- request: TranslationAuthorizationRequest,
47
- ): Promise<boolean> {
48
- const resolver = state.resolver;
49
- if (!resolver) return false;
50
- try {
51
- return (await resolver(request)) === true;
52
- } catch (error) {
53
- console.warn('[translation] authorization resolver failed; denying.', error);
54
- return false;
55
- }
56
- }
package/src/backend.ts DELETED
@@ -1,2 +0,0 @@
1
- // The backend loader instantiates every named export; export Providers only.
2
- export { TranslationProvider } from './shapes/TranslationProvider.js';
package/src/cdn-loader.ts DELETED
@@ -1,264 +0,0 @@
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 `translation: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 host-application 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
- }