@bleedingdev/modern-js-i18n-runtime-extensions 3.9.3-ultramodern.7 → 3.9.3-ultramodern.9

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/src/native.ts CHANGED
@@ -7,6 +7,8 @@ import i18next, {
7
7
  type BackendModule,
8
8
  type i18n as I18nInstance,
9
9
  type InitOptions,
10
+ type Module,
11
+ type NewableModule,
10
12
  } from 'i18next';
11
13
  import { createLatestLanguageSyncBinding } from './language-sync/controller';
12
14
  import {
@@ -23,6 +25,387 @@ import { createI18nUrlRewrite } from './urlRewrite';
23
25
 
24
26
  export type { I18nInstance };
25
27
 
28
+ interface NativeSnapshotOrigin {
29
+ instance: I18nInstance;
30
+ original: InitOptions;
31
+ seed: InitOptions;
32
+ unchanged(): boolean;
33
+ }
34
+
35
+ const nativeSnapshotOrigins = new WeakMap<object, NativeSnapshotOrigin>();
36
+
37
+ /** Retire the native proof when its owning request or plugin contract ends. */
38
+ export function invalidateNativeI18nSnapshot(instance: unknown): void {
39
+ if (typeof instance === 'object' && instance !== null)
40
+ nativeSnapshotOrigins.delete(instance);
41
+ }
42
+
43
+ export interface NativeI18nRewriteSnapshot {
44
+ language: string;
45
+ languages: readonly string[];
46
+ localisedUrls?: LocalisedUrlsOption;
47
+ }
48
+
49
+ const nativeRewriteOrigins = new WeakMap<
50
+ object,
51
+ () => NativeI18nRewriteSnapshot
52
+ >();
53
+
54
+ /** Only rewrites created by the native entry have a portable routing profile. */
55
+ export function getNativeI18nRewriteSnapshot(
56
+ rewrite: unknown,
57
+ ): NativeI18nRewriteSnapshot | undefined {
58
+ return typeof rewrite === 'object' && rewrite !== null
59
+ ? nativeRewriteOrigins.get(rewrite)?.()
60
+ : undefined;
61
+ }
62
+
63
+ function detachLocalisedUrls(
64
+ value: LocalisedUrlsOption | undefined,
65
+ ): LocalisedUrlsOption | undefined {
66
+ if (value === undefined || typeof value === 'boolean') return value;
67
+ return Object.fromEntries(
68
+ Object.entries(value).map(([path, languages]) => [
69
+ path,
70
+ Object.fromEntries(Object.entries(languages)),
71
+ ]),
72
+ );
73
+ }
74
+
75
+ function plainRecord(value: unknown): value is Record<string, unknown> {
76
+ return (
77
+ typeof value === 'object' &&
78
+ value !== null &&
79
+ (Object.getPrototypeOf(value) === Object.prototype ||
80
+ Object.getPrototypeOf(value) === null)
81
+ );
82
+ }
83
+
84
+ /** Detach authored options without executing accessors or copying class state. */
85
+ function detachOption(value: unknown, ancestors = new Set<object>()): unknown {
86
+ if (value === null || typeof value !== 'object') return value;
87
+ if (
88
+ ancestors.has(value) ||
89
+ (Array.isArray(value)
90
+ ? Object.getPrototypeOf(value) !== Array.prototype
91
+ : !plainRecord(value))
92
+ )
93
+ throw new Error('Native i18n snapshot options must be plain acyclic data');
94
+ ancestors.add(value);
95
+ const copy: Record<string, unknown> | unknown[] = Array.isArray(value)
96
+ ? []
97
+ : {};
98
+ for (const key of Reflect.ownKeys(value)) {
99
+ if (Array.isArray(value) && key === 'length') continue;
100
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
101
+ if (typeof key !== 'string' || !descriptor || !('value' in descriptor))
102
+ throw new Error('Native i18n snapshot options cannot contain accessors');
103
+ Object.defineProperty(copy, key, {
104
+ value: detachOption(descriptor.value, ancestors),
105
+ enumerable: descriptor.enumerable,
106
+ writable: true,
107
+ configurable: true,
108
+ });
109
+ }
110
+ ancestors.delete(value);
111
+ return copy;
112
+ }
113
+
114
+ function detachInitOptions(options: InitOptions): InitOptions {
115
+ const copy = detachOption(options);
116
+ if (!plainRecord(copy)) throw new Error('Invalid native i18n options');
117
+ return copy;
118
+ }
119
+
120
+ /** Compare descriptors, including native default functions, without calling them. */
121
+ function descriptorProof(
122
+ object: object,
123
+ ignore: ReadonlySet<string> = new Set(),
124
+ ancestors = new Set<object>(),
125
+ ): () => boolean {
126
+ if (ancestors.has(object)) return () => true;
127
+ ancestors.add(object);
128
+ const keys = Reflect.ownKeys(object).filter(
129
+ key => typeof key !== 'string' || !ignore.has(key),
130
+ );
131
+ const proofs = keys.map(key => {
132
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
133
+ if (!descriptor || !('value' in descriptor))
134
+ throw new Error(
135
+ 'Native i18n snapshot provenance cannot contain accessors',
136
+ );
137
+ const value: unknown = descriptor.value;
138
+ const nested =
139
+ Array.isArray(value) || plainRecord(value)
140
+ ? descriptorProof(value, new Set(), ancestors)
141
+ : () => true;
142
+ return () => {
143
+ const current = Object.getOwnPropertyDescriptor(object, key);
144
+ return (
145
+ current !== undefined &&
146
+ 'value' in current &&
147
+ current.value === value &&
148
+ current.enumerable === descriptor.enumerable &&
149
+ current.configurable === descriptor.configurable &&
150
+ current.writable === descriptor.writable &&
151
+ nested()
152
+ );
153
+ };
154
+ });
155
+ ancestors.delete(object);
156
+ return () => {
157
+ const currentKeys = Reflect.ownKeys(object).filter(
158
+ key => typeof key !== 'string' || !ignore.has(key),
159
+ );
160
+ return (
161
+ currentKeys.length === keys.length &&
162
+ currentKeys.every((key, index) => key === keys[index]) &&
163
+ proofs.every(proof => proof())
164
+ );
165
+ };
166
+ }
167
+
168
+ function methodProof(object: object): () => boolean {
169
+ const prototype = Object.getPrototypeOf(object);
170
+ const methods = new Map<PropertyKey, unknown>();
171
+ for (
172
+ let current: object | null = object;
173
+ current && current !== Object.prototype;
174
+ current = Object.getPrototypeOf(current)
175
+ ) {
176
+ for (const key of Reflect.ownKeys(current)) {
177
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
178
+ if (!descriptor || !('value' in descriptor)) continue;
179
+ if (typeof descriptor.value === 'function' && !methods.has(key))
180
+ methods.set(key, descriptor.value);
181
+ }
182
+ }
183
+ return () => {
184
+ if (Object.getPrototypeOf(object) !== prototype) return false;
185
+ for (const [key, method] of methods) {
186
+ let current: object | null = object;
187
+ while (current) {
188
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
189
+ if (descriptor) {
190
+ if (!('value' in descriptor) || descriptor.value !== method)
191
+ return false;
192
+ break;
193
+ }
194
+ current = Object.getPrototypeOf(current);
195
+ }
196
+ if (!current) return false;
197
+ }
198
+ return true;
199
+ };
200
+ }
201
+
202
+ function ownValue(object: object, key: string): unknown {
203
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
204
+ if (descriptor && !('value' in descriptor))
205
+ throw new Error('Native i18n snapshot provenance cannot contain accessors');
206
+ return descriptor?.value;
207
+ }
208
+
209
+ function semanticProof(
210
+ object: object,
211
+ fields: readonly string[],
212
+ ): () => boolean {
213
+ const proofs = fields.map(key => {
214
+ const value = ownValue(object, key);
215
+ const nested =
216
+ Array.isArray(value) || plainRecord(value)
217
+ ? descriptorProof(value, new Set(['resources']))
218
+ : () => true;
219
+ if (value instanceof RegExp) {
220
+ const source = value.source;
221
+ const flags = value.flags;
222
+ return () => {
223
+ const current = ownValue(object, key);
224
+ return (
225
+ current instanceof RegExp &&
226
+ current.source === source &&
227
+ current.flags === flags
228
+ );
229
+ };
230
+ }
231
+ return () => ownValue(object, key) === value && nested();
232
+ });
233
+ return () => proofs.every(proof => proof());
234
+ }
235
+
236
+ function recordNativeSnapshotOrigin(
237
+ instance: I18nInstance,
238
+ original: InitOptions,
239
+ seed: InitOptions,
240
+ ) {
241
+ try {
242
+ const useDescriptor = Object.getOwnPropertyDescriptor(instance, 'use');
243
+ if (!useDescriptor || !('value' in useDescriptor))
244
+ throw new Error('Native plugin registration requires an own method');
245
+ const use = instance.use;
246
+ Object.defineProperty(instance, 'use', {
247
+ ...useDescriptor,
248
+ value: function registerPlugin<T extends Module>(
249
+ plugin: T | NewableModule<T>,
250
+ ) {
251
+ // Postprocessors register globally without appearing in `modules`.
252
+ // Record the public registration call instead of reading that registry.
253
+ invalidateNativeI18nSnapshot(instance);
254
+ return use(plugin);
255
+ },
256
+ });
257
+ const services: object = instance.services;
258
+ const store = instance.store;
259
+ const options = instance.options;
260
+ const modules = instance.modules;
261
+ const translator: unknown = Object.getOwnPropertyDescriptor(
262
+ instance,
263
+ 'translator',
264
+ )?.value;
265
+ const formatter: unknown = instance.services.formatter;
266
+ const formats: unknown =
267
+ formatter && typeof formatter === 'object'
268
+ ? Object.getOwnPropertyDescriptor(formatter, 'formats')?.value
269
+ : undefined;
270
+ const proofs = [
271
+ descriptorProof(instance.options, new Set(['resources'])),
272
+ descriptorProof(instance.modules),
273
+ descriptorProof(services),
274
+ methodProof(instance),
275
+ methodProof(instance.store),
276
+ ...Object.values(instance.services)
277
+ .filter(value => typeof value === 'object' && value !== null)
278
+ .map(value => methodProof(value)),
279
+ ...(translator && typeof translator === 'object'
280
+ ? [methodProof(translator)]
281
+ : []),
282
+ ...(formats && typeof formats === 'object'
283
+ ? [descriptorProof(formats)]
284
+ : []),
285
+ ];
286
+ const serviceFields: Record<string, readonly string[]> = {
287
+ languageUtils: ['options', 'supportedLngs'],
288
+ pluralResolver: ['languageUtils', 'options'],
289
+ interpolator: [
290
+ 'options',
291
+ 'escape',
292
+ 'escapeValue',
293
+ 'useRawValueToEscape',
294
+ 'prefix',
295
+ 'suffix',
296
+ 'format',
297
+ 'formatSeparator',
298
+ 'unescapePrefix',
299
+ 'unescapeSuffix',
300
+ 'nestingPrefix',
301
+ 'nestingSuffix',
302
+ 'nestingOptionsSeparator',
303
+ 'maxReplaces',
304
+ 'alwaysFormat',
305
+ 'regexp',
306
+ 'regexpUnescape',
307
+ 'nestingRegexp',
308
+ ],
309
+ formatter: ['formatSeparator'],
310
+ resourceStore: ['options'],
311
+ backendConnector: ['backend', 'store', 'services', 'options'],
312
+ };
313
+ const links = Object.entries(instance.services).map(([key, value]) => {
314
+ if (typeof value === 'object' && value !== null)
315
+ proofs.push(semanticProof(value, serviceFields[key] ?? []));
316
+ return () => ownValue(services, key) === value;
317
+ });
318
+ if (translator && typeof translator === 'object')
319
+ proofs.push(
320
+ semanticProof(translator, [
321
+ 'options',
322
+ 'resourceStore',
323
+ 'languageUtils',
324
+ 'pluralResolver',
325
+ 'interpolator',
326
+ 'backendConnector',
327
+ 'i18nFormat',
328
+ 'utils',
329
+ ]),
330
+ );
331
+ nativeSnapshotOrigins.set(instance, {
332
+ instance,
333
+ original: detachInitOptions(original),
334
+ seed: detachInitOptions(seed),
335
+ unchanged: () =>
336
+ ownValue(instance, 'services') === services &&
337
+ ownValue(instance, 'store') === store &&
338
+ ownValue(instance, 'options') === options &&
339
+ ownValue(instance, 'modules') === modules &&
340
+ ownValue(services, 'formatter') === formatter &&
341
+ (formatter && typeof formatter === 'object'
342
+ ? ownValue(formatter, 'formats') === formats
343
+ : true) &&
344
+ Object.getOwnPropertyDescriptor(instance, 'translator')?.value ===
345
+ translator &&
346
+ (translator && typeof translator === 'object'
347
+ ? ownValue(translator, 'language') === ownValue(instance, 'language')
348
+ : true) &&
349
+ links.every(proof => proof()) &&
350
+ proofs.every(proof => proof()),
351
+ });
352
+ } catch {
353
+ // Existing native applications may use opaque options. They need a named
354
+ // Worker profile, while ordinary rendering keeps its existing behavior.
355
+ }
356
+ }
357
+
358
+ /** A real fully bundled instance for request-local synchronous providers. */
359
+ export function createNativeI18nInstance(
360
+ initOptions: InitOptions,
361
+ ): I18nInstance {
362
+ const original = detachInitOptions(initOptions);
363
+ const seed: InitOptions = {
364
+ ...detachInitOptions(original),
365
+ initAsync: false,
366
+ partialBundledLanguages: false,
367
+ interpolation: { escapeValue: false, ...original.interpolation },
368
+ };
369
+ const instance = i18next.createInstance();
370
+ void instance.init(detachInitOptions(seed));
371
+ if (!instance.isInitialized || instance.isInitializing)
372
+ throw new Error('Native i18n instances require complete bundled resources');
373
+ recordNativeSnapshotOrigin(instance, original, seed);
374
+ return instance;
375
+ }
376
+
377
+ /** The original effective seed, only while the initialized native proof holds. */
378
+ export function getNativeI18nSnapshotSeed(instance: unknown): InitOptions {
379
+ const origin =
380
+ typeof instance === 'object' && instance !== null
381
+ ? nativeSnapshotOrigins.get(instance)
382
+ : undefined;
383
+ const services = origin ? ownValue(origin.instance, 'services') : undefined;
384
+ const connector =
385
+ services && typeof services === 'object'
386
+ ? ownValue(services, 'backendConnector')
387
+ : undefined;
388
+ const state =
389
+ connector && typeof connector === 'object'
390
+ ? ownValue(connector, 'state')
391
+ : undefined;
392
+ const pending =
393
+ plainRecord(state) &&
394
+ Object.values(Object.getOwnPropertyDescriptors(state)).some(
395
+ descriptor => !('value' in descriptor) || descriptor.value === 1,
396
+ );
397
+ if (
398
+ !origin ||
399
+ ownValue(origin.instance, 'isInitialized') !== true ||
400
+ ownValue(origin.instance, 'isInitializing') === true ||
401
+ ownValue(origin.instance, 'isLanguageChangingTo') ||
402
+ pending ||
403
+ !origin.unchanged()
404
+ )
405
+ throw new Error('This i18n instance requires an explicit Worker profile');
406
+ return detachInitOptions(origin.seed);
407
+ }
408
+
26
409
  export interface NativeI18nOptions extends ResolveRequestLanguageOptions {
27
410
  /**
28
411
  * JSON-serializable i18next `init` options. `lng`, `fallbackLng`,
@@ -106,7 +489,7 @@ export function createNativeI18n(
106
489
  // The default export resolves under both i18next module formats.
107
490
  const instance = i18next.createInstance();
108
491
  instance.use(backend);
109
- await instance.init({
492
+ const seed: InitOptions = {
110
493
  ...initOptions,
111
494
  lng: language,
112
495
  fallbackLng: routing.fallbackLanguage,
@@ -116,15 +499,44 @@ export function createNativeI18n(
116
499
  partialBundledLanguages: true,
117
500
  resources: resources ? { [language]: resources } : {},
118
501
  interpolation: { escapeValue: false, ...initOptions.interpolation },
119
- });
502
+ };
503
+ let original: InitOptions | undefined;
504
+ let detachedSeed: InitOptions | undefined;
505
+ try {
506
+ original = detachInitOptions(initOptions);
507
+ detachedSeed = detachInitOptions(seed);
508
+ } catch {
509
+ // Opaque custom options remain supported by normal native rendering.
510
+ }
511
+ await instance.init(seed);
512
+ if (original && detachedSeed)
513
+ recordNativeSnapshotOrigin(instance, original, detachedSeed);
120
514
  return instance;
121
515
  },
122
- rewrite: getLanguage =>
123
- createI18nUrlRewrite({
124
- languages: routing.languages,
516
+ rewrite: getLanguage => {
517
+ const languages = [...routing.languages];
518
+ const localisedUrls = detachLocalisedUrls(routing.localisedUrls);
519
+ const rewrite = createI18nUrlRewrite({
520
+ languages,
125
521
  getLanguage,
126
- localisedUrls: routing.localisedUrls,
127
- }),
522
+ localisedUrls,
523
+ });
524
+ const unchanged = descriptorProof(rewrite);
525
+ nativeRewriteOrigins.set(rewrite, () => {
526
+ if (!unchanged()) throw new Error('Native i18n rewrite was modified');
527
+ const language = getLanguage();
528
+ if (!languages.includes(language))
529
+ throw new Error('Native i18n rewrite language is unsupported');
530
+ return {
531
+ language,
532
+ languages: [...languages],
533
+ ...(localisedUrls === undefined
534
+ ? {}
535
+ : { localisedUrls: detachLocalisedUrls(localisedUrls) }),
536
+ };
537
+ });
538
+ return rewrite;
539
+ },
128
540
  handoff(language, instance) {
129
541
  return createI18nSsrHandoffInlineData({
130
542
  language,