@_linked/translation 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/lib/cjs/data/translation.json +8 -0
- package/lib/esm/archive.d.ts +133 -0
- package/lib/esm/archive.js +567 -0
- package/lib/esm/archive.js.map +1 -0
- package/lib/esm/authorization.d.ts +12 -0
- package/lib/esm/authorization.js +10 -0
- package/lib/esm/authorization.js.map +1 -0
- package/lib/esm/backend.d.ts +1 -0
- package/lib/esm/backend.js +3 -0
- package/lib/esm/backend.js.map +1 -0
- package/lib/esm/cdn-loader.d.ts +40 -0
- package/lib/esm/cdn-loader.js +200 -0
- package/lib/esm/cdn-loader.js.map +1 -0
- package/lib/esm/checks.d.ts +56 -0
- package/lib/esm/checks.js +288 -0
- package/lib/esm/checks.js.map +1 -0
- package/lib/esm/compile.d.ts +64 -0
- package/lib/esm/compile.js +99 -0
- package/lib/esm/compile.js.map +1 -0
- package/lib/esm/core/messages.d.ts +25 -0
- package/lib/esm/core/messages.js +76 -0
- package/lib/esm/core/messages.js.map +1 -0
- package/lib/esm/data/translation.json +8 -0
- package/lib/esm/discovery.d.ts +97 -0
- package/lib/esm/discovery.js +365 -0
- package/lib/esm/discovery.js.map +1 -0
- package/lib/esm/exchange.d.ts +100 -0
- package/lib/esm/exchange.js +223 -0
- package/lib/esm/exchange.js.map +1 -0
- package/lib/esm/export.d.ts +28 -0
- package/lib/esm/export.js +92 -0
- package/lib/esm/export.js.map +1 -0
- package/lib/esm/formats/csv.d.ts +8 -0
- package/lib/esm/formats/csv.js +397 -0
- package/lib/esm/formats/csv.js.map +1 -0
- package/lib/esm/formats/json.d.ts +30 -0
- package/lib/esm/formats/json.js +459 -0
- package/lib/esm/formats/json.js.map +1 -0
- package/lib/esm/formats/xliff.d.ts +12 -0
- package/lib/esm/formats/xliff.js +577 -0
- package/lib/esm/formats/xliff.js.map +1 -0
- package/lib/esm/import.d.ts +197 -0
- package/lib/esm/import.js +391 -0
- package/lib/esm/import.js.map +1 -0
- package/lib/esm/index.d.ts +42 -0
- package/lib/esm/index.js +34 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/key-sync-node.d.ts +29 -0
- package/lib/esm/key-sync-node.js +174 -0
- package/lib/esm/key-sync-node.js.map +1 -0
- package/lib/esm/key-sync.d.ts +46 -0
- package/lib/esm/key-sync.js +114 -0
- package/lib/esm/key-sync.js.map +1 -0
- package/lib/esm/key-version.d.ts +50 -0
- package/lib/esm/key-version.js +134 -0
- package/lib/esm/key-version.js.map +1 -0
- package/lib/esm/languages.d.ts +18 -0
- package/lib/esm/languages.js +74 -0
- package/lib/esm/languages.js.map +1 -0
- package/lib/esm/linked-discovery.d.ts +44 -0
- package/lib/esm/linked-discovery.js +267 -0
- package/lib/esm/linked-discovery.js.map +1 -0
- package/lib/esm/message-format.d.ts +41 -0
- package/lib/esm/message-format.js +133 -0
- package/lib/esm/message-format.js.map +1 -0
- package/lib/esm/mt.d.ts +111 -0
- package/lib/esm/mt.js +142 -0
- package/lib/esm/mt.js.map +1 -0
- package/lib/esm/ontologies/translation.d.ts +203 -0
- package/lib/esm/ontologies/translation.js +219 -0
- package/lib/esm/ontologies/translation.js.map +1 -0
- package/lib/esm/package.d.ts +4 -0
- package/lib/esm/package.js +11 -0
- package/lib/esm/package.js.map +1 -0
- package/lib/esm/react.d.ts +55 -0
- package/lib/esm/react.js +127 -0
- package/lib/esm/react.js.map +1 -0
- package/lib/esm/records.d.ts +151 -0
- package/lib/esm/records.js +2 -0
- package/lib/esm/records.js.map +1 -0
- package/lib/esm/release.d.ts +198 -0
- package/lib/esm/release.js +435 -0
- package/lib/esm/release.js.map +1 -0
- package/lib/esm/shape-catalog.d.ts +79 -0
- package/lib/esm/shape-catalog.js +118 -0
- package/lib/esm/shape-catalog.js.map +1 -0
- package/lib/esm/shapes/GlossaryTerm.d.ts +28 -0
- package/lib/esm/shapes/GlossaryTerm.js +94 -0
- package/lib/esm/shapes/GlossaryTerm.js.map +1 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.d.ts +33 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.js +209 -0
- package/lib/esm/shapes/TranslationInventoryOrigin.js.map +1 -0
- package/lib/esm/shapes/TranslationKey.d.ts +92 -0
- package/lib/esm/shapes/TranslationKey.js +194 -0
- package/lib/esm/shapes/TranslationKey.js.map +1 -0
- package/lib/esm/shapes/TranslationKeyVersion.d.ts +19 -0
- package/lib/esm/shapes/TranslationKeyVersion.js +125 -0
- package/lib/esm/shapes/TranslationKeyVersion.js.map +1 -0
- package/lib/esm/shapes/TranslationLanguage.d.ts +15 -0
- package/lib/esm/shapes/TranslationLanguage.js +102 -0
- package/lib/esm/shapes/TranslationLanguage.js.map +1 -0
- package/lib/esm/shapes/TranslationProvider.d.ts +409 -0
- package/lib/esm/shapes/TranslationProvider.js +1376 -0
- package/lib/esm/shapes/TranslationProvider.js.map +1 -0
- package/lib/esm/shapes/TranslationRelease.d.ts +36 -0
- package/lib/esm/shapes/TranslationRelease.js +149 -0
- package/lib/esm/shapes/TranslationRelease.js.map +1 -0
- package/lib/esm/shapes/TranslationRevision.d.ts +39 -0
- package/lib/esm/shapes/TranslationRevision.js +159 -0
- package/lib/esm/shapes/TranslationRevision.js.map +1 -0
- package/lib/esm/shapes/TranslationUnit.d.ts +26 -0
- package/lib/esm/shapes/TranslationUnit.js +111 -0
- package/lib/esm/shapes/TranslationUnit.js.map +1 -0
- package/package.json +134 -0
- package/src/archive.ts +894 -0
- package/src/authorization.ts +33 -0
- package/src/backend.ts +2 -0
- package/src/cdn-loader.ts +264 -0
- package/src/checks.ts +413 -0
- package/src/compile.ts +168 -0
- package/src/core/messages.ts +106 -0
- package/src/data/translation.json +8 -0
- package/src/discovery.ts +594 -0
- package/src/exchange.ts +428 -0
- package/src/export.ts +135 -0
- package/src/formats/csv.ts +459 -0
- package/src/formats/json.ts +587 -0
- package/src/formats/xliff.ts +708 -0
- package/src/import.ts +648 -0
- package/src/index.ts +207 -0
- package/src/key-sync-node.ts +269 -0
- package/src/key-sync.ts +168 -0
- package/src/key-version.ts +206 -0
- package/src/languages.ts +102 -0
- package/src/linked-discovery.ts +385 -0
- package/src/message-format.ts +214 -0
- package/src/mt.ts +248 -0
- package/src/ontologies/translation.ts +234 -0
- package/src/package.ts +19 -0
- package/src/react.tsx +250 -0
- package/src/records.ts +180 -0
- package/src/release.ts +726 -0
- package/src/shape-catalog.ts +198 -0
- package/src/shapes/GlossaryTerm.ts +61 -0
- package/src/shapes/TranslationInventoryOrigin.ts +131 -0
- package/src/shapes/TranslationKey.ts +174 -0
- package/src/shapes/TranslationKeyVersion.ts +77 -0
- package/src/shapes/TranslationLanguage.ts +51 -0
- package/src/shapes/TranslationProvider.ts +1755 -0
- package/src/shapes/TranslationRelease.ts +114 -0
- package/src/shapes/TranslationRevision.ts +105 -0
- package/src/shapes/TranslationUnit.ts +72 -0
|
@@ -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,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
|
+
}
|