@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.10
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/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +421 -0
- package/dist/cjs/entry-application.js +88 -0
- package/dist/cjs/entry-client.js +95 -0
- package/dist/cjs/entry-server.js +135 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +205 -0
- package/dist/cjs/i18n/I18nProvider.js +59 -0
- package/dist/cjs/i18n/LocalizedLink.js +80 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +83 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +122 -0
- package/dist/cjs/plugin/create.js +266 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +91 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-external-snapshot.js +291 -0
- package/dist/cjs/router-handler.js +171 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +100 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +190 -0
- package/dist/cjs/server.js +245 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/cjs/worker-federation-context.js +63 -0
- package/dist/cjs/worker-federation-contexts.js +97 -0
- package/dist/cjs/worker-federation.js +97 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +377 -0
- package/dist/esm/entry-application.mjs +44 -0
- package/dist/esm/entry-client.mjs +57 -0
- package/dist/esm/entry-server.mjs +91 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +167 -0
- package/dist/esm/i18n/I18nProvider.mjs +21 -0
- package/dist/esm/i18n/LocalizedLink.mjs +42 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-external-snapshot.mjs +244 -0
- package/dist/esm/router-handler.mjs +133 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +11 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +204 -0
- package/dist/esm/worker-federation-context.mjs +19 -0
- package/dist/esm/worker-federation-contexts.mjs +47 -0
- package/dist/esm/worker-federation.mjs +59 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +67 -0
- package/dist/esm-node/plugin/create.mjs +228 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +53 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +40 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +10 -0
- package/dist/types/entry-server.d.ts +14 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +20 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +14 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +11 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +20 -0
- package/dist/types/server.d.ts +47 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/dist/types/worker-federation-context.d.ts +34 -0
- package/dist/types/worker-federation-contexts.d.ts +25 -0
- package/dist/types/worker-federation.d.ts +20 -0
- package/package.json +278 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +568 -0
- package/src/entry-application.ts +76 -0
- package/src/entry-client.ts +90 -0
- package/src/entry-server.ts +153 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +307 -0
- package/src/i18n/I18nProvider.ts +47 -0
- package/src/i18n/LocalizedLink.ts +86 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +31 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +204 -0
- package/src/plugin/compiler.ts +104 -0
- package/src/plugin/create.ts +273 -0
- package/src/plugin/index.ts +107 -0
- package/src/plugin/profile.ts +50 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-external-snapshot.ts +363 -0
- package/src/router-handler.ts +185 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +21 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +274 -0
- package/src/server.ts +340 -0
- package/src/typecheck.ts +723 -0
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/README.md +0 -3
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNativeI18n,
|
|
3
|
+
createNativeI18nInstance,
|
|
4
|
+
getNativeI18nRewriteSnapshot,
|
|
5
|
+
getNativeI18nSnapshotSeed,
|
|
6
|
+
invalidateNativeI18nSnapshot,
|
|
7
|
+
type NativeI18nRewriteSnapshot,
|
|
8
|
+
} from '@modern-js/i18n-runtime-extensions/native';
|
|
9
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
10
|
+
import type { NativeLocationRewrite } from '@modern-js/renderer-core/router';
|
|
11
|
+
import i18next, { type i18n as I18nInstance, type Resource } from 'i18next';
|
|
12
|
+
import { I18nContext } from './i18n/context';
|
|
13
|
+
import type { I18nContextValue, I18nInstanceLike } from './i18n/types';
|
|
14
|
+
import {
|
|
15
|
+
OCTANE_WORKER_CONTEXT_KEYS,
|
|
16
|
+
registerOctaneWorkerContext,
|
|
17
|
+
registerOctaneWorkerRouterRewrite,
|
|
18
|
+
} from './worker-federation-contexts';
|
|
19
|
+
|
|
20
|
+
export type I18nSnapshotJson =
|
|
21
|
+
| null
|
|
22
|
+
| boolean
|
|
23
|
+
| number
|
|
24
|
+
| string
|
|
25
|
+
| I18nSnapshotJson[]
|
|
26
|
+
| I18nSnapshotOptions;
|
|
27
|
+
|
|
28
|
+
export interface I18nSnapshotOptions {
|
|
29
|
+
[key: string]: I18nSnapshotJson;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OctaneI18nProfileId {
|
|
33
|
+
key: string;
|
|
34
|
+
version: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OctaneI18nSnapshot {
|
|
38
|
+
version: 1;
|
|
39
|
+
profile: OctaneI18nProfileId;
|
|
40
|
+
language: string;
|
|
41
|
+
languages: string[];
|
|
42
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
43
|
+
options: I18nSnapshotOptions;
|
|
44
|
+
resources: Resource;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Named profiles install their custom translation functions locally. */
|
|
48
|
+
export interface OctaneI18nSnapshotProfile extends OctaneI18nProfileId {
|
|
49
|
+
captureOptions(instance: I18nInstanceLike): Readonly<I18nSnapshotOptions>;
|
|
50
|
+
validateOptions(options: Readonly<I18nSnapshotOptions>): void;
|
|
51
|
+
create(input: {
|
|
52
|
+
language: string;
|
|
53
|
+
options: Readonly<I18nSnapshotOptions>;
|
|
54
|
+
resources: Resource;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}): I18nInstance | Promise<I18nInstance>;
|
|
57
|
+
dispose?(instance: I18nInstance): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultProfile: OctaneI18nProfileId = {
|
|
61
|
+
key: 'ultramodern.i18next',
|
|
62
|
+
version: '1',
|
|
63
|
+
};
|
|
64
|
+
const profiles = new Map<string, OctaneI18nSnapshotProfile>();
|
|
65
|
+
const associations = new WeakMap<
|
|
66
|
+
object,
|
|
67
|
+
{
|
|
68
|
+
id: OctaneI18nProfileId;
|
|
69
|
+
profile: OctaneI18nSnapshotProfile;
|
|
70
|
+
}
|
|
71
|
+
>();
|
|
72
|
+
const restored = new WeakSet<object>();
|
|
73
|
+
const disposals = new WeakMap<object, () => void>();
|
|
74
|
+
const disposedContexts = new WeakSet<object>();
|
|
75
|
+
const retiredInstances = new WeakSet<object>();
|
|
76
|
+
|
|
77
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
78
|
+
const MAX_NODES = 100_000;
|
|
79
|
+
const MAX_DEPTH = 32;
|
|
80
|
+
const MAX_OPTIONS_BYTES = 64 * 1024;
|
|
81
|
+
|
|
82
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
83
|
+
return (
|
|
84
|
+
typeof value === 'object' &&
|
|
85
|
+
value !== null &&
|
|
86
|
+
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
87
|
+
Object.getPrototypeOf(value) === null)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Validate before copying, without invoking a publisher-supplied accessor. */
|
|
92
|
+
function copyJson(value: unknown, maxBytes = MAX_BYTES): I18nSnapshotJson {
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
let nodes = 0;
|
|
95
|
+
const ancestors = new Set<object>();
|
|
96
|
+
const charge = (value: string, quoted = true) => {
|
|
97
|
+
if (value.length > maxBytes)
|
|
98
|
+
throw new Error('i18n snapshot exceeds its budget');
|
|
99
|
+
bytes += new TextEncoder().encode(
|
|
100
|
+
quoted ? JSON.stringify(value) : value,
|
|
101
|
+
).byteLength;
|
|
102
|
+
if (bytes > maxBytes) throw new Error('i18n snapshot exceeds its budget');
|
|
103
|
+
};
|
|
104
|
+
const visit = (input: unknown, depth: number): I18nSnapshotJson => {
|
|
105
|
+
if (++nodes > MAX_NODES || depth > MAX_DEPTH)
|
|
106
|
+
throw new Error('i18n snapshot exceeds its structural budget');
|
|
107
|
+
if (input === null || typeof input === 'boolean') {
|
|
108
|
+
charge(String(input), false);
|
|
109
|
+
return input;
|
|
110
|
+
}
|
|
111
|
+
if (typeof input === 'number') {
|
|
112
|
+
if (!Number.isFinite(input))
|
|
113
|
+
throw new Error('i18n snapshot numbers must be finite');
|
|
114
|
+
charge(String(input), false);
|
|
115
|
+
return input;
|
|
116
|
+
}
|
|
117
|
+
if (typeof input === 'string') {
|
|
118
|
+
charge(input);
|
|
119
|
+
return input;
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
typeof input !== 'object' ||
|
|
123
|
+
input === null ||
|
|
124
|
+
(Array.isArray(input)
|
|
125
|
+
? Object.getPrototypeOf(input) !== Array.prototype
|
|
126
|
+
: !record(input))
|
|
127
|
+
)
|
|
128
|
+
throw new Error('i18n snapshots contain only plain JSON data');
|
|
129
|
+
if (ancestors.has(input))
|
|
130
|
+
throw new Error('i18n snapshots cannot contain cycles');
|
|
131
|
+
ancestors.add(input);
|
|
132
|
+
const result: I18nSnapshotOptions | I18nSnapshotJson[] = Array.isArray(
|
|
133
|
+
input,
|
|
134
|
+
)
|
|
135
|
+
? []
|
|
136
|
+
: {};
|
|
137
|
+
const keys = Reflect.ownKeys(input);
|
|
138
|
+
if (
|
|
139
|
+
Array.isArray(input) &&
|
|
140
|
+
(input.length > MAX_NODES ||
|
|
141
|
+
keys.length !== input.length + 1 ||
|
|
142
|
+
keys.slice(0, -1).some((key, index) => key !== String(index)))
|
|
143
|
+
)
|
|
144
|
+
throw new Error('i18n snapshot arrays must be dense');
|
|
145
|
+
charge(Array.isArray(input) ? '[]' : '{}', false);
|
|
146
|
+
let entries = 0;
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
if (Array.isArray(input) && key === 'length') continue;
|
|
149
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
150
|
+
if (
|
|
151
|
+
typeof key !== 'string' ||
|
|
152
|
+
key === '__proto__' ||
|
|
153
|
+
key === 'prototype' ||
|
|
154
|
+
key === 'constructor' ||
|
|
155
|
+
!descriptor ||
|
|
156
|
+
!descriptor.enumerable ||
|
|
157
|
+
!('value' in descriptor)
|
|
158
|
+
)
|
|
159
|
+
throw new Error(
|
|
160
|
+
'i18n snapshots cannot contain special properties or accessors',
|
|
161
|
+
);
|
|
162
|
+
if (entries++) charge(',', false);
|
|
163
|
+
if (!Array.isArray(input)) {
|
|
164
|
+
charge(key);
|
|
165
|
+
charge(':', false);
|
|
166
|
+
}
|
|
167
|
+
Object.defineProperty(result, key, {
|
|
168
|
+
value: visit(descriptor.value, depth + 1),
|
|
169
|
+
enumerable: true,
|
|
170
|
+
configurable: true,
|
|
171
|
+
writable: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
ancestors.delete(input);
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
177
|
+
return visit(value, 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function jsonRecord(value: unknown, maxBytes = MAX_BYTES): I18nSnapshotOptions {
|
|
181
|
+
const copied = copyJson(value, maxBytes);
|
|
182
|
+
if (!record(copied)) throw new Error('i18n snapshot must contain an object');
|
|
183
|
+
return copied;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function canonicalJson(value: I18nSnapshotJson): I18nSnapshotJson {
|
|
187
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
188
|
+
if (value !== null && typeof value === 'object') {
|
|
189
|
+
const result: I18nSnapshotOptions = {};
|
|
190
|
+
for (const key of Object.keys(value).sort())
|
|
191
|
+
result[key] = canonicalJson(value[key]);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sameResourcesKey(value: Resource): string {
|
|
198
|
+
return JSON.stringify(canonicalJson(copyJson(value)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function exactKeys(value: object, allowed: readonly string[]) {
|
|
202
|
+
if (Object.keys(value).some(key => !allowed.includes(key)))
|
|
203
|
+
throw new Error('Unknown i18n snapshot field');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function language(value: unknown): string {
|
|
207
|
+
if (typeof value !== 'string' || !value || value.length > 128)
|
|
208
|
+
throw new Error('Invalid i18n snapshot language');
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function profileId(value: unknown): OctaneI18nProfileId {
|
|
213
|
+
if (!record(value)) throw new Error('Invalid i18n snapshot profile');
|
|
214
|
+
exactKeys(value, ['key', 'version']);
|
|
215
|
+
const key = language(value.key);
|
|
216
|
+
const version = language(value.version);
|
|
217
|
+
return { key, version };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function profileKey(profile: OctaneI18nProfileId): string {
|
|
221
|
+
return JSON.stringify([profile.key, profile.version]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function genuineInstance(value: unknown): value is I18nInstance {
|
|
225
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
226
|
+
for (const key of ['options', 'services', 'store', 'language']) {
|
|
227
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
228
|
+
if (!descriptor || !('value' in descriptor)) return false;
|
|
229
|
+
}
|
|
230
|
+
const services: unknown = Object.getOwnPropertyDescriptor(
|
|
231
|
+
value,
|
|
232
|
+
'services',
|
|
233
|
+
)?.value;
|
|
234
|
+
const connector: unknown =
|
|
235
|
+
services && typeof services === 'object'
|
|
236
|
+
? Object.getOwnPropertyDescriptor(services, 'backendConnector')?.value
|
|
237
|
+
: undefined;
|
|
238
|
+
const state: unknown =
|
|
239
|
+
connector && typeof connector === 'object'
|
|
240
|
+
? Object.getOwnPropertyDescriptor(connector, 'state')?.value
|
|
241
|
+
: undefined;
|
|
242
|
+
const pending =
|
|
243
|
+
record(state) &&
|
|
244
|
+
Object.values(Object.getOwnPropertyDescriptors(state)).some(
|
|
245
|
+
descriptor => !('value' in descriptor) || descriptor.value === 1,
|
|
246
|
+
);
|
|
247
|
+
return (
|
|
248
|
+
Object.getPrototypeOf(value) === Object.getPrototypeOf(i18next) &&
|
|
249
|
+
Object.getOwnPropertyDescriptor(value, 'isInitialized')?.value === true &&
|
|
250
|
+
Object.getOwnPropertyDescriptor(value, 'isInitializing')?.value !== true &&
|
|
251
|
+
!Object.getOwnPropertyDescriptor(value, 'isLanguageChangingTo')?.value &&
|
|
252
|
+
typeof Object.getOwnPropertyDescriptor(value, 'language')?.value ===
|
|
253
|
+
'string' &&
|
|
254
|
+
!pending
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function stringList(value: unknown, allowFalse = false): boolean {
|
|
259
|
+
return (
|
|
260
|
+
(allowFalse && value === false) ||
|
|
261
|
+
typeof value === 'string' ||
|
|
262
|
+
(Array.isArray(value) && value.every(item => typeof item === 'string'))
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const booleanOptions = new Set([
|
|
267
|
+
'debug',
|
|
268
|
+
'nonExplicitSupportedLngs',
|
|
269
|
+
'lowerCaseLng',
|
|
270
|
+
'cleanCode',
|
|
271
|
+
'ignoreJSONStructure',
|
|
272
|
+
'returnNull',
|
|
273
|
+
'returnEmptyString',
|
|
274
|
+
'returnObjects',
|
|
275
|
+
'returnDetails',
|
|
276
|
+
'appendNamespaceToMissingKey',
|
|
277
|
+
'appendNamespaceToCIMode',
|
|
278
|
+
'cacheInBuiltFormats',
|
|
279
|
+
]);
|
|
280
|
+
const stringOptions = new Set(['pluralSeparator', 'contextSeparator']);
|
|
281
|
+
const interpolationBooleans = new Set([
|
|
282
|
+
'alwaysFormat',
|
|
283
|
+
'escapeValue',
|
|
284
|
+
'useRawValueToEscape',
|
|
285
|
+
'skipOnVariables',
|
|
286
|
+
]);
|
|
287
|
+
const interpolationStrings = new Set([
|
|
288
|
+
'formatSeparator',
|
|
289
|
+
'prefix',
|
|
290
|
+
'suffix',
|
|
291
|
+
'prefixEscaped',
|
|
292
|
+
'suffixEscaped',
|
|
293
|
+
'unescapeSuffix',
|
|
294
|
+
'unescapePrefix',
|
|
295
|
+
'nestingPrefix',
|
|
296
|
+
'nestingSuffix',
|
|
297
|
+
'nestingPrefixEscaped',
|
|
298
|
+
'nestingSuffixEscaped',
|
|
299
|
+
'nestingOptionsSeparator',
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
function validatePortableOptions(options: I18nSnapshotOptions) {
|
|
303
|
+
for (const [key, value] of Object.entries(options)) {
|
|
304
|
+
let valid = false;
|
|
305
|
+
if (booleanOptions.has(key)) valid = typeof value === 'boolean';
|
|
306
|
+
else if (stringOptions.has(key)) valid = typeof value === 'string';
|
|
307
|
+
else if (['keySeparator', 'nsSeparator', 'joinArrays'].includes(key))
|
|
308
|
+
valid = value === false || typeof value === 'string';
|
|
309
|
+
else if (['ns', 'defaultNS', 'fallbackNS'].includes(key))
|
|
310
|
+
valid = stringList(value, key !== 'ns');
|
|
311
|
+
else if (key === 'preload')
|
|
312
|
+
valid = value === false || (Array.isArray(value) && stringList(value));
|
|
313
|
+
else if (key === 'supportedLngs')
|
|
314
|
+
valid = value === false || (Array.isArray(value) && stringList(value));
|
|
315
|
+
else if (key === 'fallbackLng')
|
|
316
|
+
valid =
|
|
317
|
+
stringList(value, true) ||
|
|
318
|
+
(record(value) &&
|
|
319
|
+
Object.values(value).every(
|
|
320
|
+
item => Array.isArray(item) && stringList(item),
|
|
321
|
+
));
|
|
322
|
+
else if (key === 'load')
|
|
323
|
+
valid =
|
|
324
|
+
typeof value === 'string' &&
|
|
325
|
+
['all', 'currentOnly', 'languageOnly'].includes(value);
|
|
326
|
+
else if (key === 'enableSelector')
|
|
327
|
+
valid =
|
|
328
|
+
typeof value === 'boolean' ||
|
|
329
|
+
value === 'optimize' ||
|
|
330
|
+
value === 'strict';
|
|
331
|
+
else if (key === 'compatibilityJSON') valid = value === 'v4';
|
|
332
|
+
else if (key === 'postProcess')
|
|
333
|
+
valid = value === false || (Array.isArray(value) && value.length === 0);
|
|
334
|
+
else if (['saveMissing', 'updateMissing'].includes(key))
|
|
335
|
+
valid = value === false;
|
|
336
|
+
else if (key === 'interpolation' && record(value)) {
|
|
337
|
+
valid = Object.entries(value).every(([name, field]) => {
|
|
338
|
+
if (interpolationBooleans.has(name)) return typeof field === 'boolean';
|
|
339
|
+
if (interpolationStrings.has(name)) return typeof field === 'string';
|
|
340
|
+
if (name === 'defaultVariables') return record(field);
|
|
341
|
+
return (
|
|
342
|
+
name === 'maxReplaces' &&
|
|
343
|
+
typeof field === 'number' &&
|
|
344
|
+
Number.isSafeInteger(field) &&
|
|
345
|
+
field >= 0 &&
|
|
346
|
+
field <= 100_000
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (!valid)
|
|
351
|
+
throw new Error(`i18n option ${key} requires an explicit Worker profile`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resources(value: unknown): Resource {
|
|
356
|
+
const copied = jsonRecord(value);
|
|
357
|
+
if (Object.keys(copied).length > 64)
|
|
358
|
+
throw new Error('Too many i18n snapshot languages');
|
|
359
|
+
const result: Resource = {};
|
|
360
|
+
for (const [lng, namespaces] of Object.entries(copied)) {
|
|
361
|
+
language(lng);
|
|
362
|
+
if (!record(namespaces) || Object.keys(namespaces).length > 256)
|
|
363
|
+
throw new Error('Invalid i18n snapshot namespaces');
|
|
364
|
+
const bundles: Resource[string] = {};
|
|
365
|
+
for (const [namespace, bundle] of Object.entries(namespaces)) {
|
|
366
|
+
language(namespace);
|
|
367
|
+
if (typeof bundle !== 'string' && !record(bundle))
|
|
368
|
+
throw new Error('Invalid i18n snapshot resource bundle');
|
|
369
|
+
bundles[namespace] = bundle;
|
|
370
|
+
}
|
|
371
|
+
result[lng] = bundles;
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function loadedResources(instance: I18nInstance): Resource {
|
|
377
|
+
const result: Resource = {};
|
|
378
|
+
const descriptor = Object.getOwnPropertyDescriptor(instance.store, 'data');
|
|
379
|
+
if (!descriptor || !('value' in descriptor))
|
|
380
|
+
throw new Error('Loaded i18n resources cannot use accessors');
|
|
381
|
+
const loaded = jsonRecord(descriptor.value);
|
|
382
|
+
// Enumerate all loaded languages, including configured preloads and fallbacks.
|
|
383
|
+
for (const lng of Object.keys(loaded)) {
|
|
384
|
+
const data = jsonRecord(instance.getDataByLanguage(lng));
|
|
385
|
+
const bundles: Resource[string] = {};
|
|
386
|
+
for (const namespace of Object.keys(data)) {
|
|
387
|
+
const bundle: unknown = instance.getResourceBundle(lng, namespace);
|
|
388
|
+
if (typeof bundle !== 'string' && !record(bundle))
|
|
389
|
+
throw new Error('Invalid loaded i18n resource bundle');
|
|
390
|
+
bundles[namespace] = bundle;
|
|
391
|
+
}
|
|
392
|
+
result[lng] = bundles;
|
|
393
|
+
}
|
|
394
|
+
return resources(result);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function localizedUrls(value: unknown): LocalisedUrlsOption | undefined {
|
|
398
|
+
if (value === undefined || typeof value === 'boolean') return value;
|
|
399
|
+
if (!record(value)) throw new Error('Invalid i18n snapshot localized URLs');
|
|
400
|
+
const result: Exclude<LocalisedUrlsOption, boolean> = {};
|
|
401
|
+
for (const [path, map] of Object.entries(value)) {
|
|
402
|
+
if (!record(map))
|
|
403
|
+
throw new Error('Invalid i18n snapshot localized URL map');
|
|
404
|
+
const paths: Record<string, string> = {};
|
|
405
|
+
for (const [lng, target] of Object.entries(map)) {
|
|
406
|
+
language(lng);
|
|
407
|
+
if (typeof target !== 'string')
|
|
408
|
+
throw new Error('Invalid localized URL target');
|
|
409
|
+
paths[lng] = target;
|
|
410
|
+
}
|
|
411
|
+
result[path] = paths;
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function rewriteSnapshot(value: unknown): NativeI18nRewriteSnapshot {
|
|
417
|
+
const input = jsonRecord(value, MAX_OPTIONS_BYTES);
|
|
418
|
+
exactKeys(input, ['language', 'languages', 'localisedUrls']);
|
|
419
|
+
const lng = language(input.language);
|
|
420
|
+
if (
|
|
421
|
+
!Array.isArray(input.languages) ||
|
|
422
|
+
!input.languages.length ||
|
|
423
|
+
input.languages.length > 64
|
|
424
|
+
)
|
|
425
|
+
throw new Error('Invalid i18n rewrite languages');
|
|
426
|
+
const languages = input.languages.map(language);
|
|
427
|
+
if (new Set(languages).size !== languages.length || !languages.includes(lng))
|
|
428
|
+
throw new Error('Invalid i18n rewrite language selection');
|
|
429
|
+
const urls = localizedUrls(input.localisedUrls);
|
|
430
|
+
return {
|
|
431
|
+
language: lng,
|
|
432
|
+
languages,
|
|
433
|
+
...(urls === undefined ? {} : { localisedUrls: urls }),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function captureOctaneI18nRewriteSnapshot(
|
|
438
|
+
rewrite: NativeLocationRewrite,
|
|
439
|
+
): NativeI18nRewriteSnapshot {
|
|
440
|
+
const snapshot = getNativeI18nRewriteSnapshot(rewrite);
|
|
441
|
+
if (!snapshot)
|
|
442
|
+
throw new Error('Unknown i18n router rewrite requires a named profile');
|
|
443
|
+
return rewriteSnapshot(snapshot);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function restoreOctaneI18nRewriteSnapshot(
|
|
447
|
+
value: unknown,
|
|
448
|
+
): NativeLocationRewrite {
|
|
449
|
+
const snapshot = rewriteSnapshot(value);
|
|
450
|
+
return createNativeI18n(
|
|
451
|
+
{
|
|
452
|
+
languages: snapshot.languages,
|
|
453
|
+
fallbackLanguage: snapshot.language,
|
|
454
|
+
localisedUrls: snapshot.localisedUrls,
|
|
455
|
+
},
|
|
456
|
+
{},
|
|
457
|
+
).rewrite(() => snapshot.language);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function registerOctaneI18nSnapshotProfile(
|
|
461
|
+
profile: OctaneI18nSnapshotProfile,
|
|
462
|
+
): () => void {
|
|
463
|
+
const id = profileId({ key: profile.key, version: profile.version });
|
|
464
|
+
if (
|
|
465
|
+
typeof profile.captureOptions !== 'function' ||
|
|
466
|
+
typeof profile.validateOptions !== 'function' ||
|
|
467
|
+
typeof profile.create !== 'function' ||
|
|
468
|
+
(profile.dispose !== undefined && typeof profile.dispose !== 'function')
|
|
469
|
+
)
|
|
470
|
+
throw new Error('Invalid i18n Worker profile implementation');
|
|
471
|
+
const key = profileKey(id);
|
|
472
|
+
if (key === profileKey(defaultProfile) || profiles.has(key))
|
|
473
|
+
throw new Error('Duplicate i18n Worker profile');
|
|
474
|
+
const captured: OctaneI18nSnapshotProfile = Object.freeze({
|
|
475
|
+
...id,
|
|
476
|
+
captureOptions: profile.captureOptions,
|
|
477
|
+
validateOptions: profile.validateOptions,
|
|
478
|
+
create: profile.create,
|
|
479
|
+
...(profile.dispose ? { dispose: profile.dispose } : {}),
|
|
480
|
+
});
|
|
481
|
+
profiles.set(key, captured);
|
|
482
|
+
return () => {
|
|
483
|
+
if (profiles.get(key) === captured) profiles.delete(key);
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function associateOctaneI18nSnapshotProfile(
|
|
488
|
+
instance: I18nInstanceLike,
|
|
489
|
+
id: OctaneI18nProfileId,
|
|
490
|
+
) {
|
|
491
|
+
const copied = profileId(jsonRecord(id));
|
|
492
|
+
const profile = profiles.get(profileKey(copied));
|
|
493
|
+
if (!genuineInstance(instance) || retiredInstances.has(instance))
|
|
494
|
+
throw new Error('Profiles require a live genuine i18n instance');
|
|
495
|
+
if (!profile)
|
|
496
|
+
throw new Error('The exact i18n Worker profile must be registered locally');
|
|
497
|
+
if (restored.has(instance)) {
|
|
498
|
+
const association = associations.get(instance);
|
|
499
|
+
if (
|
|
500
|
+
association?.profile !== profile ||
|
|
501
|
+
profileKey(association.id) !== profileKey(copied)
|
|
502
|
+
)
|
|
503
|
+
throw new Error('A restored i18n context retains its owning profile');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
associations.set(instance, { id: copied, profile });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Built-in translation behavior only. Custom per-call processors need a profile. */
|
|
510
|
+
export function captureOctaneI18nSnapshot(
|
|
511
|
+
value: I18nContextValue,
|
|
512
|
+
): OctaneI18nSnapshot {
|
|
513
|
+
const instance = value.instance;
|
|
514
|
+
if (disposedContexts.has(value) || retiredInstances.has(instance))
|
|
515
|
+
throw new Error('Cannot capture a disposed i18n Worker context');
|
|
516
|
+
if (!genuineInstance(instance))
|
|
517
|
+
throw new Error(
|
|
518
|
+
'Worker i18n snapshots require a genuine settled i18next instance',
|
|
519
|
+
);
|
|
520
|
+
if (instance.language !== value.language)
|
|
521
|
+
throw new Error('Worker i18n context language must be settled');
|
|
522
|
+
const association = associations.get(instance);
|
|
523
|
+
const selected = association?.id ?? defaultProfile;
|
|
524
|
+
let options: I18nSnapshotOptions;
|
|
525
|
+
if (profileKey(selected) === profileKey(defaultProfile)) {
|
|
526
|
+
const seed = getNativeI18nSnapshotSeed(instance);
|
|
527
|
+
const { lng, resources, initAsync, partialBundledLanguages, ...portable } =
|
|
528
|
+
seed;
|
|
529
|
+
options = jsonRecord(portable, MAX_OPTIONS_BYTES);
|
|
530
|
+
validatePortableOptions(options);
|
|
531
|
+
} else {
|
|
532
|
+
const profile = profiles.get(profileKey(selected));
|
|
533
|
+
if (!profile || association?.profile !== profile)
|
|
534
|
+
throw new Error('Missing or changed exact i18n Worker profile');
|
|
535
|
+
options = jsonRecord(profile.captureOptions(instance), MAX_OPTIONS_BYTES);
|
|
536
|
+
profile.validateOptions(options);
|
|
537
|
+
}
|
|
538
|
+
return parseSnapshot({
|
|
539
|
+
version: 1,
|
|
540
|
+
profile: selected,
|
|
541
|
+
language: value.language,
|
|
542
|
+
languages: [...value.languages],
|
|
543
|
+
...(value.localisedUrls === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { localisedUrls: value.localisedUrls }),
|
|
546
|
+
options,
|
|
547
|
+
resources: loadedResources(instance),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function parseSnapshot(value: unknown): OctaneI18nSnapshot {
|
|
552
|
+
const input = jsonRecord(value);
|
|
553
|
+
exactKeys(input, [
|
|
554
|
+
'version',
|
|
555
|
+
'profile',
|
|
556
|
+
'language',
|
|
557
|
+
'languages',
|
|
558
|
+
'localisedUrls',
|
|
559
|
+
'options',
|
|
560
|
+
'resources',
|
|
561
|
+
]);
|
|
562
|
+
if (input.version !== 1) throw new Error('Unsupported i18n snapshot version');
|
|
563
|
+
const lng = language(input.language);
|
|
564
|
+
if (
|
|
565
|
+
!Array.isArray(input.languages) ||
|
|
566
|
+
!input.languages.length ||
|
|
567
|
+
input.languages.length > 64
|
|
568
|
+
)
|
|
569
|
+
throw new Error('Invalid i18n snapshot supported languages');
|
|
570
|
+
const languages = input.languages.map(language);
|
|
571
|
+
if (new Set(languages).size !== languages.length || !languages.includes(lng))
|
|
572
|
+
throw new Error('Invalid i18n snapshot language selection');
|
|
573
|
+
const options = jsonRecord(input.options, MAX_OPTIONS_BYTES);
|
|
574
|
+
const urls = localizedUrls(input.localisedUrls);
|
|
575
|
+
return {
|
|
576
|
+
version: 1,
|
|
577
|
+
profile: profileId(input.profile),
|
|
578
|
+
language: lng,
|
|
579
|
+
languages,
|
|
580
|
+
...(urls === undefined ? {} : { localisedUrls: urls }),
|
|
581
|
+
options,
|
|
582
|
+
resources: resources(input.resources),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function throwIfAborted(signal?: AbortSignal) {
|
|
587
|
+
signal?.throwIfAborted();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function disposeInstance(instance: I18nInstance) {
|
|
591
|
+
for (const event of [
|
|
592
|
+
'initialized',
|
|
593
|
+
'loaded',
|
|
594
|
+
'failedLoading',
|
|
595
|
+
'languageChanging',
|
|
596
|
+
'languageChanged',
|
|
597
|
+
'missingKey',
|
|
598
|
+
'*',
|
|
599
|
+
])
|
|
600
|
+
instance.off(event);
|
|
601
|
+
instance.store.off('added');
|
|
602
|
+
instance.store.off('removed');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function retireInstance(instance: I18nInstance) {
|
|
606
|
+
if (
|
|
607
|
+
typeof instance !== 'object' ||
|
|
608
|
+
instance === null ||
|
|
609
|
+
Object.getPrototypeOf(instance) !== Object.getPrototypeOf(i18next)
|
|
610
|
+
)
|
|
611
|
+
return;
|
|
612
|
+
retiredInstances.add(instance);
|
|
613
|
+
associations.delete(instance);
|
|
614
|
+
invalidateNativeI18nSnapshot(instance);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function cleanupFreshInstance(instance: I18nInstance, dispose?: () => void) {
|
|
618
|
+
if (restored.has(instance)) return;
|
|
619
|
+
retireInstance(instance);
|
|
620
|
+
if (genuineInstance(instance)) disposeInstance(instance);
|
|
621
|
+
dispose?.();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function createProfileInstance(
|
|
625
|
+
profile: OctaneI18nSnapshotProfile,
|
|
626
|
+
snapshot: OctaneI18nSnapshot,
|
|
627
|
+
signal?: AbortSignal,
|
|
628
|
+
): Promise<I18nInstance> {
|
|
629
|
+
// A profile owns partial allocations until it returns. If it returns after
|
|
630
|
+
// cancellation, this codec disposes that completed instance immediately.
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
let finished = false;
|
|
633
|
+
const abort = () => {
|
|
634
|
+
if (finished) return;
|
|
635
|
+
finished = true;
|
|
636
|
+
signal?.removeEventListener('abort', abort);
|
|
637
|
+
reject(signal?.reason ?? new Error('i18n restoration aborted'));
|
|
638
|
+
};
|
|
639
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
640
|
+
if (signal?.aborted) abort();
|
|
641
|
+
Promise.resolve()
|
|
642
|
+
.then(() => {
|
|
643
|
+
if (finished)
|
|
644
|
+
throw signal?.reason ?? new Error('i18n restoration aborted');
|
|
645
|
+
return profile.create({
|
|
646
|
+
language: snapshot.language,
|
|
647
|
+
options: snapshot.options,
|
|
648
|
+
resources: resources(snapshot.resources),
|
|
649
|
+
...(signal ? { signal } : {}),
|
|
650
|
+
});
|
|
651
|
+
})
|
|
652
|
+
.then(
|
|
653
|
+
instance => {
|
|
654
|
+
if (finished) {
|
|
655
|
+
try {
|
|
656
|
+
cleanupFreshInstance(instance, () => profile.dispose?.(instance));
|
|
657
|
+
} catch {
|
|
658
|
+
// The request already rejected; the local profile owns cleanup errors.
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
finished = true;
|
|
663
|
+
signal?.removeEventListener('abort', abort);
|
|
664
|
+
resolve(instance);
|
|
665
|
+
},
|
|
666
|
+
error => {
|
|
667
|
+
if (finished) return;
|
|
668
|
+
finished = true;
|
|
669
|
+
signal?.removeEventListener('abort', abort);
|
|
670
|
+
reject(error);
|
|
671
|
+
},
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export async function restoreOctaneI18nSnapshot(
|
|
677
|
+
value: unknown,
|
|
678
|
+
signal?: AbortSignal,
|
|
679
|
+
): Promise<I18nContextValue> {
|
|
680
|
+
throwIfAborted(signal);
|
|
681
|
+
const snapshot = parseSnapshot(value);
|
|
682
|
+
const expectedResources = sameResourcesKey(snapshot.resources);
|
|
683
|
+
let instance: I18nInstance;
|
|
684
|
+
let dispose: (() => void) | undefined;
|
|
685
|
+
let restoredProfile: OctaneI18nSnapshotProfile | undefined;
|
|
686
|
+
if (profileKey(snapshot.profile) === profileKey(defaultProfile)) {
|
|
687
|
+
validatePortableOptions(snapshot.options);
|
|
688
|
+
instance = createNativeI18nInstance({
|
|
689
|
+
...snapshot.options,
|
|
690
|
+
lng: snapshot.language,
|
|
691
|
+
resources: resources(snapshot.resources),
|
|
692
|
+
initAsync: false,
|
|
693
|
+
partialBundledLanguages: false,
|
|
694
|
+
});
|
|
695
|
+
} else {
|
|
696
|
+
const profile = profiles.get(profileKey(snapshot.profile));
|
|
697
|
+
if (!profile) throw new Error('Missing exact i18n Worker profile');
|
|
698
|
+
restoredProfile = profile;
|
|
699
|
+
profile.validateOptions(snapshot.options);
|
|
700
|
+
instance = await createProfileInstance(profile, snapshot, signal);
|
|
701
|
+
if (profile.dispose) dispose = () => profile.dispose?.(instance);
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
throwIfAborted(signal);
|
|
705
|
+
if (
|
|
706
|
+
restoredProfile &&
|
|
707
|
+
profiles.get(profileKey(snapshot.profile)) !== restoredProfile
|
|
708
|
+
)
|
|
709
|
+
throw new Error('The i18n Worker profile changed during restoration');
|
|
710
|
+
if (
|
|
711
|
+
!genuineInstance(instance) ||
|
|
712
|
+
instance.language !== snapshot.language ||
|
|
713
|
+
restored.has(instance)
|
|
714
|
+
)
|
|
715
|
+
throw new Error(
|
|
716
|
+
'An i18n Worker profile must create a fresh settled instance',
|
|
717
|
+
);
|
|
718
|
+
const current = loadedResources(instance);
|
|
719
|
+
if (sameResourcesKey(current) !== expectedResources)
|
|
720
|
+
throw new Error(
|
|
721
|
+
'An i18n Worker profile must restore every loaded resource',
|
|
722
|
+
);
|
|
723
|
+
restored.add(instance);
|
|
724
|
+
if (restoredProfile)
|
|
725
|
+
associations.set(instance, {
|
|
726
|
+
id: { ...snapshot.profile },
|
|
727
|
+
profile: restoredProfile,
|
|
728
|
+
});
|
|
729
|
+
const context: I18nContextValue = {
|
|
730
|
+
instance,
|
|
731
|
+
language: snapshot.language,
|
|
732
|
+
languages: snapshot.languages,
|
|
733
|
+
...(snapshot.localisedUrls === undefined
|
|
734
|
+
? {}
|
|
735
|
+
: { localisedUrls: snapshot.localisedUrls }),
|
|
736
|
+
};
|
|
737
|
+
let disposed = false;
|
|
738
|
+
disposals.set(context, () => {
|
|
739
|
+
if (disposed) return;
|
|
740
|
+
disposed = true;
|
|
741
|
+
disposedContexts.add(context);
|
|
742
|
+
retireInstance(instance);
|
|
743
|
+
disposeInstance(instance);
|
|
744
|
+
dispose?.();
|
|
745
|
+
});
|
|
746
|
+
return context;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
cleanupFreshInstance(instance, dispose);
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function disposeOctaneI18nSnapshot(value: I18nContextValue) {
|
|
754
|
+
const dispose = disposals.get(value);
|
|
755
|
+
disposals.delete(value);
|
|
756
|
+
dispose?.();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
let registrations = 0;
|
|
760
|
+
let releaseContext: (() => void) | undefined;
|
|
761
|
+
|
|
762
|
+
export function registerOctaneI18nWorkerContexts(): () => void {
|
|
763
|
+
if (!releaseContext) {
|
|
764
|
+
const releaseRewrite = registerOctaneWorkerRouterRewrite({
|
|
765
|
+
capture: captureOctaneI18nRewriteSnapshot,
|
|
766
|
+
restore: restoreOctaneI18nRewriteSnapshot,
|
|
767
|
+
});
|
|
768
|
+
try {
|
|
769
|
+
const unregisterContext = registerOctaneWorkerContext(I18nContext, {
|
|
770
|
+
key: OCTANE_WORKER_CONTEXT_KEYS.i18n,
|
|
771
|
+
encode: value => {
|
|
772
|
+
if (value === null) throw new Error('Missing i18n context provider');
|
|
773
|
+
return captureOctaneI18nSnapshot(value);
|
|
774
|
+
},
|
|
775
|
+
decode: restoreOctaneI18nSnapshot,
|
|
776
|
+
dispose: value => {
|
|
777
|
+
if (value !== null) disposeOctaneI18nSnapshot(value);
|
|
778
|
+
},
|
|
779
|
+
isProvided: value => value !== null,
|
|
780
|
+
});
|
|
781
|
+
releaseContext = () => {
|
|
782
|
+
try {
|
|
783
|
+
unregisterContext();
|
|
784
|
+
} finally {
|
|
785
|
+
releaseRewrite();
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
} catch (error) {
|
|
789
|
+
releaseRewrite();
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
registrations++;
|
|
794
|
+
let released = false;
|
|
795
|
+
return () => {
|
|
796
|
+
if (released) return;
|
|
797
|
+
released = true;
|
|
798
|
+
if (--registrations === 0) {
|
|
799
|
+
releaseContext?.();
|
|
800
|
+
releaseContext = undefined;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|