@ccheever/exact-ibex-runtime 0.1.0

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 (161) hide show
  1. package/package.json +63 -0
  2. package/src/abort/AbortController.ts +23 -0
  3. package/src/abort/AbortSignal.ts +152 -0
  4. package/src/abort/index.ts +2 -0
  5. package/src/accessibility.ts +12 -0
  6. package/src/arraybuffer-detach.ts +109 -0
  7. package/src/base64/base64.ts +168 -0
  8. package/src/base64/index.ts +1 -0
  9. package/src/blob/Blob.ts +259 -0
  10. package/src/blob/File.ts +59 -0
  11. package/src/blob/FormData.ts +323 -0
  12. package/src/blob/index.ts +3 -0
  13. package/src/bootstrap.ts +1946 -0
  14. package/src/broadcast/BroadcastChannel.ts +280 -0
  15. package/src/broadcast/index.ts +5 -0
  16. package/src/cache/Cache.ts +349 -0
  17. package/src/cache/CacheStorage.ts +89 -0
  18. package/src/cache/index.ts +27 -0
  19. package/src/camera/index.ts +6202 -0
  20. package/src/camera/processor.worker.ts +194 -0
  21. package/src/camera/scene.ts +195 -0
  22. package/src/clipboard/Clipboard.ts +129 -0
  23. package/src/clipboard/ClipboardItem.ts +97 -0
  24. package/src/clipboard/index.ts +6 -0
  25. package/src/clone/index.ts +1 -0
  26. package/src/clone/structuredClone.ts +389 -0
  27. package/src/clone/transferableSymbols.ts +2 -0
  28. package/src/compression/CompressionStream.ts +146 -0
  29. package/src/compression/DecompressionStream.ts +342 -0
  30. package/src/compression/index.ts +4 -0
  31. package/src/console/Console.ts +341 -0
  32. package/src/console/index.ts +2 -0
  33. package/src/core/accessibility-state.ts +263 -0
  34. package/src/core/accessibility.ts +184 -0
  35. package/src/core/agent-state.ts +37 -0
  36. package/src/core/diagnostics-logs.ts +144 -0
  37. package/src/core/host-call-bridge.ts +16 -0
  38. package/src/core/i18n-helpers.ts +189 -0
  39. package/src/core/locale-state.ts +253 -0
  40. package/src/core/locale.ts +95 -0
  41. package/src/crypto/Crypto.ts +2743 -0
  42. package/src/crypto/index.ts +1 -0
  43. package/src/diagnostics/logs.ts +7 -0
  44. package/src/encoding/TextDecoder.ts +1181 -0
  45. package/src/encoding/TextDecoderStream.ts +58 -0
  46. package/src/encoding/TextEncoder.ts +180 -0
  47. package/src/encoding/TextEncoderStream.ts +39 -0
  48. package/src/encoding/index.ts +8 -0
  49. package/src/events/CloseEvent.ts +91 -0
  50. package/src/events/DOMException.ts +409 -0
  51. package/src/events/ErrorEvent.ts +39 -0
  52. package/src/events/Event.ts +151 -0
  53. package/src/events/EventTarget.ts +280 -0
  54. package/src/events/FocusEvent.ts +27 -0
  55. package/src/events/KeyboardEvent.ts +46 -0
  56. package/src/events/MessageEvent.ts +61 -0
  57. package/src/events/ProgressEvent.ts +33 -0
  58. package/src/events/PromiseRejectionEvent.ts +31 -0
  59. package/src/events/index.ts +52 -0
  60. package/src/eventsource/EventSource.ts +371 -0
  61. package/src/eventsource/index.ts +2 -0
  62. package/src/fetch/Headers.ts +642 -0
  63. package/src/fetch/Request.ts +760 -0
  64. package/src/fetch/Response.ts +543 -0
  65. package/src/fetch/body.ts +1256 -0
  66. package/src/fetch/cookie-jar.ts +566 -0
  67. package/src/fetch/demo.ts +207 -0
  68. package/src/fetch/errors.ts +101 -0
  69. package/src/fetch/fetch.ts +2610 -0
  70. package/src/fetch/index.ts +101 -0
  71. package/src/fetch/native-bridge.ts +65 -0
  72. package/src/fetch/types.ts +258 -0
  73. package/src/filereader/FileReader.ts +236 -0
  74. package/src/filereader/index.ts +1 -0
  75. package/src/fs/Dirent.ts +39 -0
  76. package/src/fs/ExactFile.ts +450 -0
  77. package/src/fs/Stats.ts +80 -0
  78. package/src/fs/index.ts +944 -0
  79. package/src/fs/promises.ts +386 -0
  80. package/src/fs/shared.ts +328 -0
  81. package/src/http-server/index.js +697 -0
  82. package/src/http-server/index.ts +27 -0
  83. package/src/identity.generated.ts +14 -0
  84. package/src/index.ts +283 -0
  85. package/src/indexeddb/IDBCursor.ts +188 -0
  86. package/src/indexeddb/IDBDatabase.ts +343 -0
  87. package/src/indexeddb/IDBFactory.ts +269 -0
  88. package/src/indexeddb/IDBIndex.ts +194 -0
  89. package/src/indexeddb/IDBKeyRange.ts +109 -0
  90. package/src/indexeddb/IDBObjectStore.ts +468 -0
  91. package/src/indexeddb/IDBRequest.ts +163 -0
  92. package/src/indexeddb/IDBTransaction.ts +207 -0
  93. package/src/indexeddb/index.ts +34 -0
  94. package/src/indexeddb/utils.ts +52 -0
  95. package/src/inspect/index.ts +1 -0
  96. package/src/inspect/inspect.ts +465 -0
  97. package/src/internal/detect.ts +104 -0
  98. package/src/locale.ts +10 -0
  99. package/src/location/index.ts +1059 -0
  100. package/src/locks/LockManager.ts +460 -0
  101. package/src/locks/index.ts +12 -0
  102. package/src/media/VideoFrame.ts +58 -0
  103. package/src/messaging/MessageChannel.ts +31 -0
  104. package/src/messaging/MessagePort.ts +180 -0
  105. package/src/messaging/index.ts +2 -0
  106. package/src/messaging.ts +247 -0
  107. package/src/native/NativeModules.ts +354 -0
  108. package/src/native/index.ts +1 -0
  109. package/src/navigator/Navigator.ts +351 -0
  110. package/src/navigator/index.ts +1 -0
  111. package/src/node/Buffer.ts +1786 -0
  112. package/src/node/index.ts +4 -0
  113. package/src/node/path.ts +495 -0
  114. package/src/node/process.ts +2528 -0
  115. package/src/performance/Performance.ts +532 -0
  116. package/src/performance/index.ts +21 -0
  117. package/src/polyfills/array.ts +236 -0
  118. package/src/polyfills/arraybuffer.ts +172 -0
  119. package/src/polyfills/groupby.ts +85 -0
  120. package/src/polyfills/index.ts +85 -0
  121. package/src/polyfills/intl.ts +1956 -0
  122. package/src/polyfills/iterator.ts +479 -0
  123. package/src/polyfills/promise.ts +37 -0
  124. package/src/polyfills/set.ts +245 -0
  125. package/src/polyfills/string.ts +85 -0
  126. package/src/polyfills/typedarray.ts +110 -0
  127. package/src/promise-rejection-tracking.ts +464 -0
  128. package/src/react-native/index.ts +388 -0
  129. package/src/runtime-entry.ts +55 -0
  130. package/src/scheduling/AnimationFrame.ts +105 -0
  131. package/src/scheduling/IdleCallback.ts +167 -0
  132. package/src/scheduling/index.ts +13 -0
  133. package/src/security/Capabilities.ts +1146 -0
  134. package/src/security/Permissions.ts +392 -0
  135. package/src/security/capability-bits.generated.ts +63 -0
  136. package/src/security/index.ts +16 -0
  137. package/src/sqlite/Database.ts +456 -0
  138. package/src/sqlite/Statement.ts +206 -0
  139. package/src/sqlite/constants.ts +79 -0
  140. package/src/sqlite/errors.ts +25 -0
  141. package/src/sqlite/index.ts +34 -0
  142. package/src/sqlite/module.js +438 -0
  143. package/src/storage/Storage.ts +291 -0
  144. package/src/storage/StorageManager.ts +91 -0
  145. package/src/storage/index.ts +3 -0
  146. package/src/stream-compat.ts +47 -0
  147. package/src/streams/ReadableStream.ts +4131 -0
  148. package/src/streams/TransformStream.ts +375 -0
  149. package/src/streams/WritableStream.ts +866 -0
  150. package/src/streams/index.ts +41 -0
  151. package/src/timers/Timers.ts +296 -0
  152. package/src/timers/index.ts +11 -0
  153. package/src/url/URL.ts +656 -0
  154. package/src/url/URLPattern.ts +850 -0
  155. package/src/url/URLSearchParams.ts +244 -0
  156. package/src/url/index.ts +9 -0
  157. package/src/websocket/WebSocket.ts +770 -0
  158. package/src/websocket/WebSocketError.ts +52 -0
  159. package/src/websocket/WebSocketStream.ts +628 -0
  160. package/src/websocket/index.ts +7 -0
  161. package/src/window/index.ts +872 -0
@@ -0,0 +1,1956 @@
1
+ /**
2
+ * Intl polyfills for Hermes engine
3
+ *
4
+ * Hermes has limited Intl support. It provides basic Intl.DateTimeFormat and
5
+ * Intl.NumberFormat, but may lack newer methods (formatRange, formatToParts)
6
+ * and entirely missing APIs like:
7
+ * - Intl.PluralRules
8
+ * - Intl.ListFormat
9
+ * - Intl.RelativeTimeFormat
10
+ * - Intl.Segmenter
11
+ * - Intl.DisplayNames
12
+ * - Intl.Collator
13
+ *
14
+ * These polyfills cover the API surface with locale-aware plural rules for
15
+ * the top 14 locales, proper formatToParts decomposition for NumberFormat
16
+ * and DateTimeFormat, improved grapheme segmentation for emoji, and more.
17
+ * They will be superseded by native implementations when available.
18
+ */
19
+
20
+ import {
21
+ canonicalizeLocaleTag,
22
+ directionForLocaleTag,
23
+ parseLocaleTag,
24
+ } from '../core/i18n-helpers.js';
25
+
26
+ const __DEV__ = process.env.NODE_ENV !== 'production';
27
+
28
+ function parseUnicodeExtensionKeyword(
29
+ localeTag: string,
30
+ keyword: string,
31
+ ): string | undefined {
32
+ const extensions = localeTag.split('-u-')[1];
33
+ if (!extensions) {
34
+ return undefined;
35
+ }
36
+
37
+ const tokens = extensions.split('-').filter(Boolean);
38
+ for (let index = 0; index < tokens.length; index += 1) {
39
+ if (tokens[index] !== keyword) {
40
+ continue;
41
+ }
42
+
43
+ const value = tokens[index + 1];
44
+ return value && value.length > 0 ? value : undefined;
45
+ }
46
+
47
+ return undefined;
48
+ }
49
+
50
+ export function installIntlPolyfills(): void {
51
+ const g = globalThis as any;
52
+
53
+ // Ensure Intl namespace exists
54
+ if (typeof g.Intl === 'undefined') {
55
+ g.Intl = {};
56
+ }
57
+
58
+ const Intl = g.Intl;
59
+
60
+ // --------------------------------------------------------------------------
61
+ // Intl.getCanonicalLocales
62
+ // Canonicalizes locale tags (lowercase language, uppercase region, titlecase script).
63
+ // --------------------------------------------------------------------------
64
+ if (typeof Intl.getCanonicalLocales !== 'function') {
65
+ Object.defineProperty(Intl, 'getCanonicalLocales', {
66
+ value: function getCanonicalLocales(locales?: string | string[]): string[] {
67
+ if (!locales) return [];
68
+ const input = typeof locales === 'string' ? [locales] : Array.from(locales);
69
+ return input.map(tag => {
70
+ const str = String(tag);
71
+ if (str.length === 0) {
72
+ throw new RangeError('invalid language tag: ""');
73
+ }
74
+ // Basic canonicalization: lowercase language, uppercase region, titlecase script
75
+ const parts = str.split('-');
76
+ if (parts.length >= 1) parts[0] = parts[0].toLowerCase();
77
+ if (parts.length >= 2) {
78
+ if (parts[1].length === 4) {
79
+ // Script subtag: titlecase
80
+ parts[1] = parts[1][0].toUpperCase() + parts[1].slice(1).toLowerCase();
81
+ } else if (parts[1].length === 2) {
82
+ // Region subtag: uppercase
83
+ parts[1] = parts[1].toUpperCase();
84
+ }
85
+ }
86
+ if (parts.length >= 3 && parts[2].length === 2) {
87
+ parts[2] = parts[2].toUpperCase();
88
+ }
89
+ return parts.join('-');
90
+ });
91
+ },
92
+ writable: true,
93
+ enumerable: false,
94
+ configurable: true,
95
+ });
96
+ }
97
+
98
+ // --------------------------------------------------------------------------
99
+ // Intl.Locale subset
100
+ // Covers algorithmic BCP 47 parsing plus text direction and unicode keyword reads.
101
+ // --------------------------------------------------------------------------
102
+ if (typeof Intl.Locale !== 'function') {
103
+ class LocalePolyfill {
104
+ private readonly _tag: string;
105
+ private readonly _baseName: string;
106
+ private readonly _parsed: ReturnType<typeof parseLocaleTag>;
107
+
108
+ constructor(tag: string) {
109
+ if (typeof tag !== 'string' || tag.trim().length === 0) {
110
+ throw new TypeError('Intl.Locale requires a non-empty locale tag');
111
+ }
112
+
113
+ this._tag = canonicalizeLocaleTag(tag);
114
+ this._baseName = this._tag.split('-u-')[0] ?? this._tag;
115
+ this._parsed = parseLocaleTag(this._tag);
116
+ }
117
+
118
+ get baseName(): string {
119
+ return this._baseName;
120
+ }
121
+
122
+ get language(): string {
123
+ return this._parsed.language;
124
+ }
125
+
126
+ get region(): string | undefined {
127
+ return this._parsed.region;
128
+ }
129
+
130
+ get script(): string | undefined {
131
+ return this._parsed.script;
132
+ }
133
+
134
+ get calendar(): string | undefined {
135
+ return parseUnicodeExtensionKeyword(this._tag, 'ca');
136
+ }
137
+
138
+ get numberingSystem(): string | undefined {
139
+ return parseUnicodeExtensionKeyword(this._tag, 'nu');
140
+ }
141
+
142
+ get textInfo(): { direction: 'ltr' | 'rtl' } {
143
+ return {
144
+ direction: directionForLocaleTag(this._tag),
145
+ };
146
+ }
147
+
148
+ maximize(): this {
149
+ if (__DEV__) {
150
+ console.warn('Intl.Locale.prototype.maximize() is not implemented in Exact core.');
151
+ }
152
+ return this;
153
+ }
154
+
155
+ minimize(): this {
156
+ if (__DEV__) {
157
+ console.warn('Intl.Locale.prototype.minimize() is not implemented in Exact core.');
158
+ }
159
+ return this;
160
+ }
161
+
162
+ toString(): string {
163
+ return this._tag;
164
+ }
165
+
166
+ toJSON(): string {
167
+ return this._tag;
168
+ }
169
+ }
170
+
171
+ Object.defineProperty(Intl, 'Locale', {
172
+ value: LocalePolyfill,
173
+ writable: true,
174
+ enumerable: false,
175
+ configurable: true,
176
+ });
177
+ } else if (
178
+ Intl.Locale?.prototype &&
179
+ !('textInfo' in Intl.Locale.prototype)
180
+ ) {
181
+ Object.defineProperty(Intl.Locale.prototype, 'textInfo', {
182
+ get(this: { toString(): string }) {
183
+ return {
184
+ direction: directionForLocaleTag(String(this)),
185
+ };
186
+ },
187
+ enumerable: false,
188
+ configurable: true,
189
+ });
190
+ }
191
+
192
+ // --------------------------------------------------------------------------
193
+ // Intl.DateTimeFormat enhancements
194
+ // Hermes may have a basic DateTimeFormat but lack formatRange/formatToParts.
195
+ // --------------------------------------------------------------------------
196
+ if (typeof Intl.DateTimeFormat === 'function') {
197
+ const DTFProto = Intl.DateTimeFormat.prototype;
198
+
199
+ // formatToParts - returns an array of parts for a formatted date
200
+ // Strategy: format individual components and find them in the full string
201
+ if (typeof DTFProto.formatToParts !== 'function') {
202
+ Object.defineProperty(DTFProto, 'formatToParts', {
203
+ value: function formatToParts(date?: Date | number): Array<{type: string; value: string}> {
204
+ const d = date === undefined ? new Date() : new Date(date);
205
+ const formatted = this.format(d);
206
+ const opts = this.resolvedOptions ? this.resolvedOptions() : {};
207
+ const locale = opts.locale || 'en';
208
+ const parts: Array<{type: string; value: string}> = [];
209
+
210
+ // Build a list of component values by formatting with single options
211
+ // Each entry: [type, value, optionKey]
212
+ type ComponentEntry = { type: string; value: string; priority: number };
213
+ const components: ComponentEntry[] = [];
214
+
215
+ // Helper to safely format with a single option
216
+ const tryFormat = (type: string, fmtOpts: any, priority: number): void => {
217
+ try {
218
+ const fmt = new Intl.DateTimeFormat(locale, fmtOpts);
219
+ const val = fmt.format(d);
220
+ if (val && val.length > 0) {
221
+ components.push({ type, value: val, priority });
222
+ }
223
+ } catch (_e) {
224
+ // If the engine doesn't support this option combo, skip
225
+ }
226
+ };
227
+
228
+ // Try each date/time component that might be in the format
229
+ // Priority determines matching order (longer/more specific first)
230
+ if (opts.era) tryFormat('era', { era: opts.era }, 10);
231
+ if (opts.year) tryFormat('year', { year: opts.year }, 20);
232
+ if (opts.month) tryFormat('month', { month: opts.month }, 15);
233
+ if (opts.day) tryFormat('day', { day: opts.day }, 30);
234
+ if (opts.weekday) tryFormat('weekday', { weekday: opts.weekday }, 5);
235
+ if (opts.hour) tryFormat('hour', { hour: opts.hour, hour12: opts.hour12 }, 40);
236
+ if (opts.minute) tryFormat('minute', { minute: opts.minute }, 50);
237
+ if (opts.second) tryFormat('second', { second: opts.second }, 60);
238
+ if (opts.dayPeriod || opts.hour12) tryFormat('dayPeriod', { hour: 'numeric', hour12: true }, 45);
239
+ if (opts.timeZoneName) tryFormat('timeZoneName', { timeZoneName: opts.timeZoneName }, 70);
240
+
241
+ // If we have no components, try the default date components
242
+ if (components.length === 0) {
243
+ tryFormat('month', { month: 'numeric' }, 15);
244
+ tryFormat('day', { day: 'numeric' }, 30);
245
+ tryFormat('year', { year: 'numeric' }, 20);
246
+ }
247
+
248
+ // Now try to locate each component in the formatted string
249
+ // Sort by value length descending so we match longer strings first
250
+ components.sort((a, b) => b.value.length - a.value.length || a.priority - b.priority);
251
+
252
+ // Track which character positions in the formatted string are assigned
253
+ const assigned = new Array(formatted.length).fill(false);
254
+ const charType = new Array(formatted.length).fill('');
255
+
256
+ for (const comp of components) {
257
+ const idx = findUnassigned(formatted, comp.value, assigned);
258
+ if (idx >= 0) {
259
+ for (let ci = idx; ci < idx + comp.value.length; ci++) {
260
+ assigned[ci] = true;
261
+ charType[ci] = comp.type;
262
+ }
263
+ }
264
+ }
265
+
266
+ // Extract dayPeriod from hour formatting (AM/PM)
267
+ // The hour format might include the period; strip it out
268
+ // by checking if any 'dayPeriod' character overlaps with 'hour'
269
+
270
+ // Build the parts array by grouping consecutive chars of same type
271
+ let currentType = '';
272
+ let currentValue = '';
273
+ for (let ci = 0; ci < formatted.length; ci++) {
274
+ const t = charType[ci] || 'literal';
275
+ if (t !== currentType) {
276
+ if (currentValue) {
277
+ parts.push({ type: currentType || 'literal', value: currentValue });
278
+ }
279
+ currentType = t;
280
+ currentValue = formatted[ci];
281
+ } else {
282
+ currentValue += formatted[ci];
283
+ }
284
+ }
285
+ if (currentValue) {
286
+ parts.push({ type: currentType || 'literal', value: currentValue });
287
+ }
288
+
289
+ // If no parts were produced (shouldn't happen), fall back
290
+ if (parts.length === 0) {
291
+ return [{ type: 'literal', value: formatted }];
292
+ }
293
+
294
+ return parts;
295
+ },
296
+ writable: true,
297
+ enumerable: false,
298
+ configurable: true,
299
+ });
300
+
301
+ function findUnassigned(str: string, substr: string, assigned: boolean[]): number {
302
+ let startIdx = 0;
303
+ while (startIdx <= str.length - substr.length) {
304
+ const idx = str.indexOf(substr, startIdx);
305
+ if (idx < 0) return -1;
306
+ // Check that none of these positions are already assigned
307
+ let allFree = true;
308
+ for (let i = idx; i < idx + substr.length; i++) {
309
+ if (assigned[i]) {
310
+ allFree = false;
311
+ break;
312
+ }
313
+ }
314
+ if (allFree) return idx;
315
+ startIdx = idx + 1;
316
+ }
317
+ return -1;
318
+ }
319
+ }
320
+
321
+ // formatRange - formats a date range
322
+ if (typeof DTFProto.formatRange !== 'function') {
323
+ Object.defineProperty(DTFProto, 'formatRange', {
324
+ value: function formatRange(startDate: Date | number, endDate: Date | number): string {
325
+ if (startDate === undefined || endDate === undefined) {
326
+ throw new TypeError('formatRange requires two dates');
327
+ }
328
+ const start = this.format(new Date(startDate));
329
+ const end = this.format(new Date(endDate));
330
+ if (start === end) return start;
331
+ return `${start} \u2013 ${end}`;
332
+ },
333
+ writable: true,
334
+ enumerable: false,
335
+ configurable: true,
336
+ });
337
+ }
338
+
339
+ // formatRangeToParts - returns parts for a formatted date range
340
+ if (typeof DTFProto.formatRangeToParts !== 'function') {
341
+ Object.defineProperty(DTFProto, 'formatRangeToParts', {
342
+ value: function formatRangeToParts(
343
+ startDate: Date | number,
344
+ endDate: Date | number,
345
+ ): Array<{type: string; value: string; source: string}> {
346
+ if (startDate === undefined || endDate === undefined) {
347
+ throw new TypeError('formatRangeToParts requires two dates');
348
+ }
349
+ const start = this.format(new Date(startDate));
350
+ const end = this.format(new Date(endDate));
351
+ if (start === end) {
352
+ return [{ type: 'literal', value: start, source: 'shared' }];
353
+ }
354
+ return [
355
+ { type: 'literal', value: start, source: 'startRange' },
356
+ { type: 'literal', value: ' \u2013 ', source: 'shared' },
357
+ { type: 'literal', value: end, source: 'endRange' },
358
+ ];
359
+ },
360
+ writable: true,
361
+ enumerable: false,
362
+ configurable: true,
363
+ });
364
+ }
365
+ }
366
+
367
+ // --------------------------------------------------------------------------
368
+ // Intl.NumberFormat enhancements
369
+ // Hermes may have basic NumberFormat but lack formatToParts/formatRange.
370
+ // --------------------------------------------------------------------------
371
+ if (typeof Intl.NumberFormat === 'function') {
372
+ const NFProto = Intl.NumberFormat.prototype;
373
+
374
+ // formatToParts - returns parts of a formatted number
375
+ // Strategy: use native format() to get the string, then decompose it
376
+ // by analyzing the resolved options (style, currency, etc.)
377
+ if (typeof NFProto.formatToParts !== 'function') {
378
+ Object.defineProperty(NFProto, 'formatToParts', {
379
+ value: function formatToParts(value?: number): Array<{type: string; value: string}> {
380
+ const num = value === undefined ? NaN : Number(value);
381
+ const formatted = this.format(num);
382
+
383
+ if (isNaN(num)) {
384
+ return [{ type: 'nan', value: formatted }];
385
+ }
386
+ if (!isFinite(num)) {
387
+ return [{ type: 'infinity', value: formatted }];
388
+ }
389
+
390
+ const opts = this.resolvedOptions ? this.resolvedOptions() : {};
391
+ const parts: Array<{type: string; value: string}> = [];
392
+
393
+ // Determine sign
394
+ const isNeg = num < 0 || (Object.is && Object.is(num, -0));
395
+ const absNum = Math.abs(num);
396
+
397
+ // Format the absolute value to get the core number string
398
+ let absFormatted: string;
399
+ try {
400
+ // Create a formatter without currency/percent to get just the number
401
+ const absOpts: any = {};
402
+ if (opts.minimumIntegerDigits) absOpts.minimumIntegerDigits = opts.minimumIntegerDigits;
403
+ if (opts.minimumFractionDigits != null) absOpts.minimumFractionDigits = opts.minimumFractionDigits;
404
+ if (opts.maximumFractionDigits != null) absOpts.maximumFractionDigits = opts.maximumFractionDigits;
405
+ if (opts.minimumSignificantDigits) absOpts.minimumSignificantDigits = opts.minimumSignificantDigits;
406
+ if (opts.maximumSignificantDigits) absOpts.maximumSignificantDigits = opts.maximumSignificantDigits;
407
+ if (opts.useGrouping != null) absOpts.useGrouping = opts.useGrouping;
408
+ const absFmt = new Intl.NumberFormat(opts.locale || 'en', absOpts);
409
+ absFormatted = absFmt.format(absNum);
410
+ } catch (_e) {
411
+ absFormatted = String(absNum);
412
+ }
413
+
414
+ // Handle different styles
415
+ const style = opts.style || 'decimal';
416
+
417
+ if (style === 'percent') {
418
+ // Percent: the formatted string includes the % sign
419
+ return _parsePercentParts(formatted, isNeg);
420
+ }
421
+
422
+ if (style === 'currency') {
423
+ return _parseCurrencyParts(formatted, absFormatted, isNeg, opts);
424
+ }
425
+
426
+ // Decimal or other: parse the number string
427
+ if (isNeg) {
428
+ // Find and remove the minus sign
429
+ const minusIdx = formatted.indexOf('-') >= 0 ? formatted.indexOf('-') : formatted.indexOf('\u2212');
430
+ if (minusIdx >= 0) {
431
+ const minusChar = formatted[minusIdx];
432
+ const before = formatted.substring(0, minusIdx);
433
+ const after = formatted.substring(minusIdx + minusChar.length);
434
+ if (before.length > 0) parts.push({ type: 'literal', value: before });
435
+ parts.push({ type: 'minusSign', value: minusChar });
436
+ const numberStr = after.trim() || after;
437
+ parts.push(..._parseDecimalParts(numberStr.length > 0 ? numberStr : absFormatted));
438
+ return parts;
439
+ }
440
+ }
441
+
442
+ return _parseDecimalParts(formatted);
443
+ },
444
+ writable: true,
445
+ enumerable: false,
446
+ configurable: true,
447
+ });
448
+
449
+ /**
450
+ * Parse a decimal number string (potentially with grouping) into parts.
451
+ * Handles patterns like: "12,345.67" or "12.345,67" (European)
452
+ *
453
+ * Known edge case: For numbers with exactly 3 digits after the last
454
+ * separator (e.g., "1.234"), the last separator is ambiguous -- it could
455
+ * be a group separator (1234) or a decimal point (1.234). We treat it
456
+ * as a decimal point (last separator = decimal), which is correct for
457
+ * most locales when a single separator is present, but may be incorrect
458
+ * for locales like German where "1.234" means one thousand two hundred
459
+ * thirty-four. This is inherent to string-based decomposition without
460
+ * knowing the locale's separator convention.
461
+ */
462
+ function _parseDecimalParts(str: string): Array<{type: string; value: string}> {
463
+ const parts: Array<{type: string; value: string}> = [];
464
+ const trimmed = str.trim();
465
+
466
+ if (trimmed.length === 0) {
467
+ return [{ type: 'integer', value: '0' }];
468
+ }
469
+
470
+ // Identify the decimal separator: it's the last non-digit punctuation
471
+ // that is followed only by digits (and possibly trailing chars)
472
+ // Common decimal separators: . , ·
473
+ // Common group separators: , . ' (space) (narrow no-break space)
474
+ let decimalPos = -1;
475
+ let decimalChar = '';
476
+
477
+ // Find the last separator character
478
+ // Walk from the end to find the decimal point
479
+ for (let i = trimmed.length - 1; i >= 0; i--) {
480
+ const ch = trimmed[i];
481
+ if (ch === '.' || ch === ',' || ch === '\u00B7') {
482
+ // Check if everything after this char (to end) is digits
483
+ const afterSep = trimmed.substring(i + 1);
484
+ if (/^\d+$/.test(afterSep)) {
485
+ decimalPos = i;
486
+ decimalChar = ch;
487
+ break;
488
+ }
489
+ }
490
+ }
491
+
492
+ let integerPart: string;
493
+ let fractionPart: string | null = null;
494
+
495
+ if (decimalPos >= 0) {
496
+ integerPart = trimmed.substring(0, decimalPos);
497
+ fractionPart = trimmed.substring(decimalPos + 1);
498
+ } else {
499
+ integerPart = trimmed;
500
+ }
501
+
502
+ // Parse the integer part, splitting on group separators
503
+ // Group separators are non-digit characters within the integer part
504
+ if (integerPart.length > 0) {
505
+ _parseIntegerWithGroups(integerPart, parts);
506
+ } else {
507
+ parts.push({ type: 'integer', value: '0' });
508
+ }
509
+
510
+ if (fractionPart !== null) {
511
+ parts.push({ type: 'decimal', value: decimalChar });
512
+ parts.push({ type: 'fraction', value: fractionPart });
513
+ }
514
+
515
+ return parts;
516
+ }
517
+
518
+ /**
519
+ * Parse integer part, identifying group separators.
520
+ */
521
+ function _parseIntegerWithGroups(str: string, parts: Array<{type: string; value: string}>): void {
522
+ // Split on any non-digit character that acts as a group separator
523
+ let current = '';
524
+ let firstGroup = true;
525
+ for (let i = 0; i < str.length; i++) {
526
+ const ch = str[i];
527
+ if (/\d/.test(ch)) {
528
+ current += ch;
529
+ } else {
530
+ // This is a group separator
531
+ if (current.length > 0) {
532
+ parts.push({ type: 'integer', value: current });
533
+ current = '';
534
+ firstGroup = false;
535
+ }
536
+ parts.push({ type: 'group', value: ch });
537
+ }
538
+ }
539
+ if (current.length > 0) {
540
+ parts.push({ type: 'integer', value: current });
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Parse a percent-formatted number string into parts.
546
+ */
547
+ function _parsePercentParts(formatted: string, isNeg: boolean): Array<{type: string; value: string}> {
548
+ const parts: Array<{type: string; value: string}> = [];
549
+
550
+ // Find the percent sign
551
+ const percentIdx = formatted.indexOf('%');
552
+ if (percentIdx < 0) {
553
+ // No % sign found, just return as literal
554
+ return [{ type: 'literal', value: formatted }];
555
+ }
556
+
557
+ let beforePercent = formatted.substring(0, percentIdx);
558
+ let afterPercent = formatted.substring(percentIdx + 1);
559
+
560
+ // Handle minus sign
561
+ if (isNeg) {
562
+ const minusIdx = beforePercent.indexOf('-') >= 0 ? beforePercent.indexOf('-') : beforePercent.indexOf('\u2212');
563
+ if (minusIdx >= 0) {
564
+ const minusChar = beforePercent[minusIdx];
565
+ const preSign = beforePercent.substring(0, minusIdx);
566
+ const postSign = beforePercent.substring(minusIdx + 1);
567
+ if (preSign.length > 0) parts.push({ type: 'literal', value: preSign });
568
+ parts.push({ type: 'minusSign', value: minusChar });
569
+ beforePercent = postSign;
570
+ }
571
+ }
572
+
573
+ // The number part is before the percent (trimmed)
574
+ const numStr = beforePercent.trim();
575
+ if (numStr.length > 0) {
576
+ parts.push(..._parseDecimalParts(numStr));
577
+ }
578
+
579
+ // Space before percent if present
580
+ const trailingSpace = beforePercent.length - beforePercent.trimEnd().length;
581
+ if (trailingSpace > 0) {
582
+ parts.push({ type: 'literal', value: beforePercent.substring(beforePercent.trimEnd().length) });
583
+ }
584
+
585
+ parts.push({ type: 'percentSign', value: '%' });
586
+
587
+ if (afterPercent.length > 0) {
588
+ parts.push({ type: 'literal', value: afterPercent });
589
+ }
590
+
591
+ return parts;
592
+ }
593
+
594
+ /**
595
+ * Parse a currency-formatted number string into parts.
596
+ */
597
+ function _parseCurrencyParts(
598
+ formatted: string,
599
+ absFormatted: string,
600
+ isNeg: boolean,
601
+ opts: any,
602
+ ): Array<{type: string; value: string}> {
603
+ const parts: Array<{type: string; value: string}> = [];
604
+ const currencyCode = opts.currency || 'USD';
605
+ const currencyDisplay = opts.currencyDisplay || 'symbol';
606
+
607
+ // Common currency symbols
608
+ const currencySymbols: Record<string, string> = {
609
+ USD: '$', EUR: '\u20AC', GBP: '\u00A3', JPY: '\u00A5', CNY: '\u00A5',
610
+ KRW: '\u20A9', INR: '\u20B9', BRL: 'R$', CAD: 'CA$', AUD: 'A$',
611
+ CHF: 'CHF', MXN: 'MX$', SEK: 'kr', NOK: 'kr', DKK: 'kr',
612
+ PLN: 'z\u0142', TRY: '\u20BA', RUB: '\u20BD', THB: '\u0E3F',
613
+ };
614
+
615
+ // Determine what the currency symbol/code looks like in the string
616
+ let currencyStr = '';
617
+ if (currencyDisplay === 'code') {
618
+ currencyStr = currencyCode;
619
+ } else if (currencyDisplay === 'name') {
620
+ // Name display is locale-dependent; hard to detect
621
+ currencyStr = '';
622
+ } else {
623
+ // symbol or narrowSymbol
624
+ currencyStr = currencySymbols[currencyCode.toUpperCase()] || currencyCode;
625
+ }
626
+
627
+ // Try to find the currency string in the formatted output
628
+ let workStr = formatted;
629
+
630
+ // Handle minus sign first
631
+ if (isNeg) {
632
+ const minusIdx = workStr.indexOf('-') >= 0 ? workStr.indexOf('-') : workStr.indexOf('\u2212');
633
+ if (minusIdx >= 0) {
634
+ const minusChar = workStr[minusIdx];
635
+ if (minusIdx > 0) {
636
+ // Something before minus - might be currency
637
+ const pre = workStr.substring(0, minusIdx);
638
+ if (pre.trim() === currencyStr && currencyStr.length > 0) {
639
+ parts.push({ type: 'currency', value: currencyStr });
640
+ if (pre.length > currencyStr.length) {
641
+ parts.push({ type: 'literal', value: pre.substring(currencyStr.length) });
642
+ }
643
+ } else if (pre.trim().length > 0) {
644
+ parts.push({ type: 'literal', value: pre });
645
+ }
646
+ }
647
+ parts.push({ type: 'minusSign', value: minusChar });
648
+ workStr = workStr.substring(minusIdx + 1);
649
+ }
650
+ }
651
+
652
+ // Find currency in remaining string
653
+ if (currencyStr.length > 0) {
654
+ const curIdx = workStr.indexOf(currencyStr);
655
+ if (curIdx >= 0) {
656
+ const before = workStr.substring(0, curIdx);
657
+ const after = workStr.substring(curIdx + currencyStr.length);
658
+
659
+ if (curIdx === 0) {
660
+ // Currency at start (e.g., "$1,234.56")
661
+ parts.push({ type: 'currency', value: currencyStr });
662
+ // There may be a space/NBSP between currency and number
663
+ if (after.length > 0 && /^[\s\u00A0\u202F]/.test(after)) {
664
+ parts.push({ type: 'literal', value: after[0] });
665
+ const numStr = after.substring(1).trim();
666
+ if (numStr.length > 0) {
667
+ parts.push(..._parseDecimalParts(numStr));
668
+ }
669
+ } else {
670
+ const numStr = after.trim();
671
+ if (numStr.length > 0) {
672
+ parts.push(..._parseDecimalParts(numStr));
673
+ }
674
+ }
675
+ } else {
676
+ // Currency at end or middle (e.g., "1.234,56 EUR" or "1,234.56kr")
677
+ const numStr = before.trim();
678
+ if (numStr.length > 0) {
679
+ parts.push(..._parseDecimalParts(numStr));
680
+ }
681
+ // Space between number and currency
682
+ if (before.length > before.trimEnd().length) {
683
+ parts.push({ type: 'literal', value: before.substring(before.trimEnd().length) });
684
+ }
685
+ parts.push({ type: 'currency', value: currencyStr });
686
+ if (after.trim().length > 0) {
687
+ parts.push({ type: 'literal', value: after });
688
+ }
689
+ }
690
+ } else {
691
+ // Currency not found in string, just parse as decimal
692
+ parts.push(..._parseDecimalParts(workStr.trim()));
693
+ }
694
+ } else {
695
+ // No known currency string, parse what we have
696
+ parts.push(..._parseDecimalParts(workStr.trim()));
697
+ }
698
+
699
+ return parts.length > 0 ? parts : [{ type: 'literal', value: formatted }];
700
+ }
701
+ }
702
+
703
+ // formatRange - formats a number range
704
+ if (typeof NFProto.formatRange !== 'function') {
705
+ Object.defineProperty(NFProto, 'formatRange', {
706
+ value: function formatRange(start: number, end: number): string {
707
+ if (start === undefined || end === undefined) {
708
+ throw new TypeError('formatRange requires two numbers');
709
+ }
710
+ const startStr = this.format(Number(start));
711
+ const endStr = this.format(Number(end));
712
+ if (startStr === endStr) return startStr;
713
+ return `${startStr}\u2013${endStr}`;
714
+ },
715
+ writable: true,
716
+ enumerable: false,
717
+ configurable: true,
718
+ });
719
+ }
720
+
721
+ // formatRangeToParts - returns parts for a formatted number range
722
+ if (typeof NFProto.formatRangeToParts !== 'function') {
723
+ Object.defineProperty(NFProto, 'formatRangeToParts', {
724
+ value: function formatRangeToParts(
725
+ start: number,
726
+ end: number,
727
+ ): Array<{type: string; value: string; source: string}> {
728
+ if (start === undefined || end === undefined) {
729
+ throw new TypeError('formatRangeToParts requires two numbers');
730
+ }
731
+ const startStr = this.format(Number(start));
732
+ const endStr = this.format(Number(end));
733
+ if (startStr === endStr) {
734
+ return [{ type: 'literal', value: startStr, source: 'shared' }];
735
+ }
736
+ return [
737
+ { type: 'literal', value: startStr, source: 'startRange' },
738
+ { type: 'literal', value: '\u2013', source: 'shared' },
739
+ { type: 'literal', value: endStr, source: 'endRange' },
740
+ ];
741
+ },
742
+ writable: true,
743
+ enumerable: false,
744
+ configurable: true,
745
+ });
746
+ }
747
+ }
748
+
749
+ // --------------------------------------------------------------------------
750
+ // Intl.PluralRules
751
+ // Provides CLDR plural rules for the top 14+ locales.
752
+ // Supports "cardinal" and "ordinal" types.
753
+ // --------------------------------------------------------------------------
754
+ if (typeof Intl.PluralRules !== 'function') {
755
+ /**
756
+ * CLDR plural rule functions per locale.
757
+ * Each function receives the absolute number and the type ('cardinal' | 'ordinal').
758
+ * Returns the plural category: 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'.
759
+ *
760
+ * Sources: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
761
+ */
762
+ const pluralRules: Record<string, (n: number, type: string) => string> = {
763
+ // English: cardinal: 1 = one, else other; ordinal: 1st, 2nd, 3rd, Nth
764
+ en: (n: number, type: string): string => {
765
+ const i = Math.floor(Math.abs(n));
766
+ if (type === 'ordinal') {
767
+ const mod10 = i % 10;
768
+ const mod100 = i % 100;
769
+ if (mod10 === 1 && mod100 !== 11) return 'one';
770
+ if (mod10 === 2 && mod100 !== 12) return 'two';
771
+ if (mod10 === 3 && mod100 !== 13) return 'few';
772
+ return 'other';
773
+ }
774
+ if (i === 1 && n === 1) return 'one';
775
+ return 'other';
776
+ },
777
+
778
+ // French: cardinal: 0 and 1 are "one", else "other"; ordinal: 1 is "one", else "other"
779
+ fr: (n: number, type: string): string => {
780
+ const i = Math.floor(Math.abs(n));
781
+ if (type === 'ordinal') {
782
+ return i === 1 ? 'one' : 'other';
783
+ }
784
+ return (i === 0 || i === 1) ? 'one' : 'other';
785
+ },
786
+
787
+ // German: same as English cardinal (1 = one, else other); ordinal all "other"
788
+ de: (n: number, type: string): string => {
789
+ if (type === 'ordinal') return 'other';
790
+ const i = Math.floor(Math.abs(n));
791
+ if (i === 1 && n === 1) return 'one';
792
+ return 'other';
793
+ },
794
+
795
+ // Spanish: 1 = one, many for large even, else other (simplified: 1 = one, else other)
796
+ es: (n: number, type: string): string => {
797
+ if (type === 'ordinal') return 'other';
798
+ return n === 1 ? 'one' : 'other';
799
+ },
800
+
801
+ // Portuguese: cardinal: 0 and 1 are "one" (Brazilian), else "other"
802
+ // (European Portuguese: only 1, but we use Brazilian as more common)
803
+ pt: (n: number, type: string): string => {
804
+ if (type === 'ordinal') return 'other';
805
+ const i = Math.floor(Math.abs(n));
806
+ return (i === 0 || i === 1) ? 'one' : 'other';
807
+ },
808
+
809
+ // Italian: cardinal: 1 = "one", else "other"; ordinal: 8, 11, 80, 800 = "many", else "other"
810
+ it: (n: number, type: string): string => {
811
+ const i = Math.floor(Math.abs(n));
812
+ if (type === 'ordinal') {
813
+ if (i === 8 || i === 11 || i === 80 || i === 800) return 'many';
814
+ return 'other';
815
+ }
816
+ if (i === 1 && n === 1) return 'one';
817
+ return 'other';
818
+ },
819
+
820
+ // Russian: complex plural rules
821
+ // cardinal: n%10=1 && n%100!=11 -> one; n%10 in 2..4 && n%100 not in 12..14 -> few;
822
+ // n%10=0 || n%10 in 5..9 || n%100 in 11..14 -> many; else other
823
+ // ordinal: all other
824
+ ru: (n: number, type: string): string => {
825
+ if (type === 'ordinal') return 'other';
826
+ const i = Math.floor(Math.abs(n));
827
+ const mod10 = i % 10;
828
+ const mod100 = i % 100;
829
+ if (mod10 === 1 && mod100 !== 11) return 'one';
830
+ if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return 'few';
831
+ if (mod10 === 0 || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 11 && mod100 <= 14)) return 'many';
832
+ return 'other';
833
+ },
834
+
835
+ // Polish: complex plural rules
836
+ // cardinal: 1 -> one; n%10 in 2..4 && n%100 not in 12..14 -> few; else many/other
837
+ // ordinal: all other
838
+ pl: (n: number, type: string): string => {
839
+ if (type === 'ordinal') return 'other';
840
+ const i = Math.floor(Math.abs(n));
841
+ if (i === 1) return 'one';
842
+ const mod10 = i % 10;
843
+ const mod100 = i % 100;
844
+ if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return 'few';
845
+ if ((mod10 === 0 || mod10 === 1) || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 12 && mod100 <= 14)) return 'many';
846
+ return 'other';
847
+ },
848
+
849
+ // Arabic: most complex, 6 categories
850
+ // 0 -> zero, 1 -> one, 2 -> two, n%100 in 3..10 -> few, n%100 in 11..99 -> many, else other
851
+ ar: (n: number, type: string): string => {
852
+ if (type === 'ordinal') return 'other';
853
+ const i = Math.floor(Math.abs(n));
854
+ if (i === 0) return 'zero';
855
+ if (i === 1) return 'one';
856
+ if (i === 2) return 'two';
857
+ const mod100 = i % 100;
858
+ if (mod100 >= 3 && mod100 <= 10) return 'few';
859
+ if (mod100 >= 11 && mod100 <= 99) return 'many';
860
+ return 'other';
861
+ },
862
+
863
+ // Japanese: no plural distinctions, always "other"
864
+ ja: (_n: number, _type: string): string => 'other',
865
+
866
+ // Korean: no plural distinctions, always "other"
867
+ ko: (_n: number, _type: string): string => 'other',
868
+
869
+ // Chinese: no plural distinctions, always "other"
870
+ zh: (_n: number, _type: string): string => 'other',
871
+
872
+ // Turkish: cardinal: 1 = "one", else "other"; ordinal: all other
873
+ tr: (n: number, type: string): string => {
874
+ if (type === 'ordinal') return 'other';
875
+ return n === 1 ? 'one' : 'other';
876
+ },
877
+
878
+ // Dutch: same as English cardinal (1 = one, else other); ordinal all other
879
+ nl: (n: number, type: string): string => {
880
+ if (type === 'ordinal') return 'other';
881
+ const i = Math.floor(Math.abs(n));
882
+ if (i === 1 && n === 1) return 'one';
883
+ return 'other';
884
+ },
885
+
886
+ // Swedish: same as English cardinal (1 = one, else other); ordinal: 1,2 = "one", else "other"
887
+ sv: (n: number, type: string): string => {
888
+ const i = Math.floor(Math.abs(n));
889
+ if (type === 'ordinal') {
890
+ const mod10 = i % 10;
891
+ const mod100 = i % 100;
892
+ if ((mod10 === 1 || mod10 === 2) && mod100 !== 11 && mod100 !== 12) return 'one';
893
+ return 'other';
894
+ }
895
+ if (i === 1 && n === 1) return 'one';
896
+ return 'other';
897
+ },
898
+ };
899
+
900
+ // Map of locale -> plural categories that this locale uses
901
+ const pluralCategories: Record<string, Record<string, string[]>> = {
902
+ en: { cardinal: ['one', 'other'], ordinal: ['one', 'two', 'few', 'other'] },
903
+ fr: { cardinal: ['one', 'other'], ordinal: ['one', 'other'] },
904
+ de: { cardinal: ['one', 'other'], ordinal: ['other'] },
905
+ es: { cardinal: ['one', 'other'], ordinal: ['other'] },
906
+ pt: { cardinal: ['one', 'other'], ordinal: ['other'] },
907
+ it: { cardinal: ['one', 'other'], ordinal: ['many', 'other'] },
908
+ ru: { cardinal: ['one', 'few', 'many', 'other'], ordinal: ['other'] },
909
+ pl: { cardinal: ['one', 'few', 'many', 'other'], ordinal: ['other'] },
910
+ ar: { cardinal: ['zero', 'one', 'two', 'few', 'many', 'other'], ordinal: ['other'] },
911
+ ja: { cardinal: ['other'], ordinal: ['other'] },
912
+ ko: { cardinal: ['other'], ordinal: ['other'] },
913
+ zh: { cardinal: ['other'], ordinal: ['other'] },
914
+ tr: { cardinal: ['one', 'other'], ordinal: ['other'] },
915
+ nl: { cardinal: ['one', 'other'], ordinal: ['other'] },
916
+ sv: { cardinal: ['one', 'other'], ordinal: ['one', 'other'] },
917
+ };
918
+
919
+ /**
920
+ * Resolve the plural rule function for a given locale.
921
+ * Falls back to the language subtag, then to English.
922
+ */
923
+ function getPluralRule(locale: string): (n: number, type: string) => string {
924
+ const key = locale.toLowerCase();
925
+ if (pluralRules[key]) return pluralRules[key];
926
+ const lang = key.split('-')[0];
927
+ if (pluralRules[lang]) return pluralRules[lang];
928
+ return pluralRules.en;
929
+ }
930
+
931
+ function getPluralCategories(locale: string, type: string): string[] {
932
+ const key = locale.toLowerCase();
933
+ const lang = key.split('-')[0];
934
+ const catMap = pluralCategories[key] || pluralCategories[lang] || pluralCategories.en;
935
+ return catMap[type] || catMap.cardinal || ['one', 'other'];
936
+ }
937
+
938
+ class PluralRulesPolyfill {
939
+ private _locale: string;
940
+ private _type: string;
941
+ private _minimumIntegerDigits: number;
942
+ private _minimumFractionDigits: number;
943
+ private _maximumFractionDigits: number;
944
+ private _rule: (n: number, type: string) => string;
945
+
946
+ constructor(locales?: string | string[], options?: any) {
947
+ const opts = options || {};
948
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
949
+ this._type = opts.type || 'cardinal';
950
+ this._minimumIntegerDigits = opts.minimumIntegerDigits || 1;
951
+ this._minimumFractionDigits = opts.minimumFractionDigits || 0;
952
+ this._maximumFractionDigits = opts.maximumFractionDigits || 3;
953
+ this._rule = getPluralRule(this._locale);
954
+ }
955
+
956
+ select(n: number): string {
957
+ return this._rule(Number(n), this._type);
958
+ }
959
+
960
+ selectRange(start: number, end: number): string {
961
+ // Per spec, returns the plural category appropriate for a range.
962
+ // Most languages use the category of the end value for ranges.
963
+ return this._rule(Number(end), this._type);
964
+ }
965
+
966
+ resolvedOptions(): any {
967
+ return {
968
+ locale: this._locale,
969
+ type: this._type,
970
+ minimumIntegerDigits: this._minimumIntegerDigits,
971
+ minimumFractionDigits: this._minimumFractionDigits,
972
+ maximumFractionDigits: this._maximumFractionDigits,
973
+ pluralCategories: getPluralCategories(this._locale, this._type),
974
+ };
975
+ }
976
+
977
+ static supportedLocalesOf(locales?: string | string[]): string[] {
978
+ // We support all locales (with English fallback)
979
+ if (!locales) return [];
980
+ const list = Array.isArray(locales) ? locales : [locales];
981
+ return list.filter((l) => typeof l === 'string');
982
+ }
983
+ }
984
+
985
+ Object.defineProperty(Intl, 'PluralRules', {
986
+ value: PluralRulesPolyfill,
987
+ writable: true,
988
+ enumerable: false,
989
+ configurable: true,
990
+ });
991
+ }
992
+
993
+ // --------------------------------------------------------------------------
994
+ // Intl.ListFormat
995
+ // Formats lists of items (e.g., "A, B, and C" or "A, B, or C").
996
+ // --------------------------------------------------------------------------
997
+ if (typeof Intl.ListFormat !== 'function') {
998
+ class ListFormatPolyfill {
999
+ private _locale: string;
1000
+ private _type: string;
1001
+ private _style: string;
1002
+
1003
+ constructor(locales?: string | string[], options?: any) {
1004
+ const opts = options || {};
1005
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1006
+ this._type = opts.type || 'conjunction'; // conjunction, disjunction, unit
1007
+ this._style = opts.style || 'long'; // long, short, narrow
1008
+ }
1009
+
1010
+ format(list: Iterable<string>): string {
1011
+ const items = Array.from(list);
1012
+ if (items.length === 0) return '';
1013
+ if (items.length === 1) return String(items[0]);
1014
+
1015
+ const conjunction = this._type === 'disjunction' ? 'or' : 'and';
1016
+
1017
+ if (this._type === 'unit') {
1018
+ // Unit type uses different separators based on style
1019
+ if (this._style === 'narrow') {
1020
+ return items.join(' ');
1021
+ }
1022
+ return items.join(', ');
1023
+ }
1024
+
1025
+ if (items.length === 2) {
1026
+ if (this._style === 'narrow') {
1027
+ return `${items[0]}, ${items[1]}`;
1028
+ }
1029
+ return `${items[0]} ${conjunction} ${items[1]}`;
1030
+ }
1031
+
1032
+ // 3+ items
1033
+ const last = items[items.length - 1];
1034
+ const rest = items.slice(0, -1);
1035
+ if (this._style === 'narrow') {
1036
+ return `${rest.join(', ')}, ${last}`;
1037
+ }
1038
+ return `${rest.join(', ')}, ${conjunction} ${last}`;
1039
+ }
1040
+
1041
+ formatToParts(list: Iterable<string>): Array<{type: string; value: string}> {
1042
+ const items = Array.from(list);
1043
+ if (items.length === 0) return [];
1044
+ if (items.length === 1) {
1045
+ return [{ type: 'element', value: String(items[0]) }];
1046
+ }
1047
+
1048
+ const conjunction = this._type === 'disjunction' ? 'or' : 'and';
1049
+ const parts: Array<{type: string; value: string}> = [];
1050
+
1051
+ for (let i = 0; i < items.length; i++) {
1052
+ if (i > 0) {
1053
+ if (i === items.length - 1) {
1054
+ if (this._type === 'unit') {
1055
+ parts.push({ type: 'literal', value: this._style === 'narrow' ? ' ' : ', ' });
1056
+ } else if (this._style === 'narrow') {
1057
+ parts.push({ type: 'literal', value: ', ' });
1058
+ } else if (items.length === 2) {
1059
+ parts.push({ type: 'literal', value: ` ${conjunction} ` });
1060
+ } else {
1061
+ parts.push({ type: 'literal', value: `, ${conjunction} ` });
1062
+ }
1063
+ } else {
1064
+ parts.push({ type: 'literal', value: ', ' });
1065
+ }
1066
+ }
1067
+ parts.push({ type: 'element', value: String(items[i]) });
1068
+ }
1069
+
1070
+ return parts;
1071
+ }
1072
+
1073
+ resolvedOptions(): any {
1074
+ return {
1075
+ locale: this._locale,
1076
+ type: this._type,
1077
+ style: this._style,
1078
+ };
1079
+ }
1080
+
1081
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1082
+ if (!locales) return [];
1083
+ const list = Array.isArray(locales) ? locales : [locales];
1084
+ return list.filter((l) => typeof l === 'string');
1085
+ }
1086
+ }
1087
+
1088
+ Object.defineProperty(Intl, 'ListFormat', {
1089
+ value: ListFormatPolyfill,
1090
+ writable: true,
1091
+ enumerable: false,
1092
+ configurable: true,
1093
+ });
1094
+ }
1095
+
1096
+ // --------------------------------------------------------------------------
1097
+ // Intl.RelativeTimeFormat
1098
+ // Formats relative time strings (e.g., "3 days ago", "in 2 hours").
1099
+ // --------------------------------------------------------------------------
1100
+ if (typeof Intl.RelativeTimeFormat !== 'function') {
1101
+ // Unit names for long/short/narrow styles
1102
+ const UNITS_LONG: Record<string, [string, string]> = {
1103
+ year: ['year', 'years'],
1104
+ quarter: ['quarter', 'quarters'],
1105
+ month: ['month', 'months'],
1106
+ week: ['week', 'weeks'],
1107
+ day: ['day', 'days'],
1108
+ hour: ['hour', 'hours'],
1109
+ minute: ['minute', 'minutes'],
1110
+ second: ['second', 'seconds'],
1111
+ };
1112
+ const UNITS_SHORT: Record<string, string> = {
1113
+ year: 'yr.',
1114
+ quarter: 'qtr.',
1115
+ month: 'mo.',
1116
+ week: 'wk.',
1117
+ day: 'day',
1118
+ hour: 'hr.',
1119
+ minute: 'min.',
1120
+ second: 'sec.',
1121
+ };
1122
+ const UNITS_NARROW: Record<string, string> = {
1123
+ year: 'y',
1124
+ quarter: 'q',
1125
+ month: 'm',
1126
+ week: 'w',
1127
+ day: 'd',
1128
+ hour: 'h',
1129
+ minute: 'm',
1130
+ second: 's',
1131
+ };
1132
+
1133
+ const VALID_UNITS = new Set([
1134
+ 'year', 'years',
1135
+ 'quarter', 'quarters',
1136
+ 'month', 'months',
1137
+ 'week', 'weeks',
1138
+ 'day', 'days',
1139
+ 'hour', 'hours',
1140
+ 'minute', 'minutes',
1141
+ 'second', 'seconds',
1142
+ ]);
1143
+
1144
+ function normalizeUnit(unit: string): string {
1145
+ if (!VALID_UNITS.has(unit)) {
1146
+ throw new RangeError(`Invalid unit argument for RelativeTimeFormat: ${unit}`);
1147
+ }
1148
+ // Remove trailing 's' for plural form
1149
+ return unit.replace(/s$/, '');
1150
+ }
1151
+
1152
+ class RelativeTimeFormatPolyfill {
1153
+ private _locale: string;
1154
+ private _style: string;
1155
+ private _numeric: string;
1156
+
1157
+ constructor(locales?: string | string[], options?: any) {
1158
+ const opts = options || {};
1159
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1160
+ this._style = opts.style || 'long';
1161
+ this._numeric = opts.numeric || 'always';
1162
+ }
1163
+
1164
+ format(value: number, unit: string): string {
1165
+ const normalizedUnit = normalizeUnit(unit);
1166
+ const n = Number(value);
1167
+
1168
+ // "auto" numeric: use special words for -1, 0, 1
1169
+ if (this._numeric === 'auto') {
1170
+ if (normalizedUnit === 'day') {
1171
+ if (n === -1) return 'yesterday';
1172
+ if (n === 0) return 'today';
1173
+ if (n === 1) return 'tomorrow';
1174
+ }
1175
+ if (n === 0) return `this ${normalizedUnit}`;
1176
+ if (n === -1) return `last ${normalizedUnit}`;
1177
+ if (n === 1) return `next ${normalizedUnit}`;
1178
+ }
1179
+
1180
+ const abs = Math.abs(n);
1181
+ let unitStr: string;
1182
+
1183
+ if (this._style === 'narrow') {
1184
+ unitStr = UNITS_NARROW[normalizedUnit] || normalizedUnit;
1185
+ if (n < 0) return `${abs}${unitStr} ago`;
1186
+ return `in ${abs}${unitStr}`;
1187
+ }
1188
+
1189
+ if (this._style === 'short') {
1190
+ unitStr = UNITS_SHORT[normalizedUnit] || normalizedUnit;
1191
+ } else {
1192
+ // long
1193
+ const names = UNITS_LONG[normalizedUnit];
1194
+ unitStr = names ? (abs === 1 ? names[0] : names[1]) : normalizedUnit;
1195
+ }
1196
+
1197
+ if (n < 0) return `${abs} ${unitStr} ago`;
1198
+ return `in ${abs} ${unitStr}`;
1199
+ }
1200
+
1201
+ formatToParts(value: number, unit: string): Array<{type: string; value: string; unit?: string}> {
1202
+ const formatted = this.format(value, unit);
1203
+ const normalizedUnit = normalizeUnit(unit);
1204
+ const n = Number(value);
1205
+
1206
+ // Simple implementation: return as literal + integer parts
1207
+ if (n < 0) {
1208
+ const abs = Math.abs(n);
1209
+ return [
1210
+ { type: 'integer', value: String(abs), unit: normalizedUnit },
1211
+ { type: 'literal', value: ' ' },
1212
+ { type: 'literal', value: formatted.replace(/^\d+\s*/, '').replace(/^\s+/, '') },
1213
+ ];
1214
+ }
1215
+ return [{ type: 'literal', value: formatted }];
1216
+ }
1217
+
1218
+ resolvedOptions(): any {
1219
+ return {
1220
+ locale: this._locale,
1221
+ style: this._style,
1222
+ numeric: this._numeric,
1223
+ numberingSystem: 'latn',
1224
+ };
1225
+ }
1226
+
1227
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1228
+ if (!locales) return [];
1229
+ const list = Array.isArray(locales) ? locales : [locales];
1230
+ return list.filter((l) => typeof l === 'string');
1231
+ }
1232
+ }
1233
+
1234
+ Object.defineProperty(Intl, 'RelativeTimeFormat', {
1235
+ value: RelativeTimeFormatPolyfill,
1236
+ writable: true,
1237
+ enumerable: false,
1238
+ configurable: true,
1239
+ });
1240
+ }
1241
+
1242
+ // --------------------------------------------------------------------------
1243
+ // Intl.Segmenter
1244
+ // Lightweight grapheme/word/sentence segmenter.
1245
+ // Handles surrogate pairs, ZWJ sequences, regional indicators, variation
1246
+ // selectors, and combining marks for proper emoji segmentation.
1247
+ // --------------------------------------------------------------------------
1248
+ if (typeof Intl.Segmenter !== 'function') {
1249
+ const WORD_REGEX = /[^\s]+|\s+/g;
1250
+ const SENTENCE_REGEX = /[^.!?]+[.!?]+\s*|[^.!?]+$/g;
1251
+
1252
+ // Unicode constants for grapheme segmentation
1253
+ const ZWJ = 0x200D;
1254
+ const VS15 = 0xFE0E; // text variation selector
1255
+ const VS16 = 0xFE0F; // emoji variation selector
1256
+ const RI_START = 0x1F1E6; // Regional Indicator A
1257
+ const RI_END = 0x1F1FF; // Regional Indicator Z
1258
+ const SKIN_TONE_START = 0x1F3FB;
1259
+ const SKIN_TONE_END = 0x1F3FF;
1260
+ // Enclosing keycap
1261
+ const KEYCAP = 0x20E3;
1262
+ // Tags block
1263
+ const TAG_START = 0xE0020;
1264
+ const TAG_END = 0xE007E;
1265
+ const CANCEL_TAG = 0xE007F;
1266
+
1267
+ /**
1268
+ * Get the code point at position i in the string.
1269
+ * Returns [codePoint, charCount] where charCount is 1 for BMP, 2 for surrogate pairs.
1270
+ */
1271
+ function codePointAt(str: string, i: number): [number, number] {
1272
+ const code = str.charCodeAt(i);
1273
+ if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
1274
+ const next = str.charCodeAt(i + 1);
1275
+ if (next >= 0xDC00 && next <= 0xDFFF) {
1276
+ const cp = ((code - 0xD800) << 10) + (next - 0xDC00) + 0x10000;
1277
+ return [cp, 2];
1278
+ }
1279
+ }
1280
+ return [code, 1];
1281
+ }
1282
+
1283
+ /**
1284
+ * Check if a code point is a regional indicator symbol.
1285
+ */
1286
+ function isRegionalIndicator(cp: number): boolean {
1287
+ return cp >= RI_START && cp <= RI_END;
1288
+ }
1289
+
1290
+ /**
1291
+ * Check if a code point is a combining mark (Unicode categories Mn, Mc, Me).
1292
+ * This is a simplified check covering the most common ranges.
1293
+ */
1294
+ function isCombiningMark(cp: number): boolean {
1295
+ // Combining Diacritical Marks (0300-036F)
1296
+ if (cp >= 0x0300 && cp <= 0x036F) return true;
1297
+ // Combining Diacritical Marks Extended (1AB0-1AFF)
1298
+ if (cp >= 0x1AB0 && cp <= 0x1AFF) return true;
1299
+ // Combining Diacritical Marks Supplement (1DC0-1DFF)
1300
+ if (cp >= 0x1DC0 && cp <= 0x1DFF) return true;
1301
+ // Combining Diacritical Marks for Symbols (20D0-20FF)
1302
+ if (cp >= 0x20D0 && cp <= 0x20FF) return true;
1303
+ // Combining Half Marks (FE20-FE2F)
1304
+ if (cp >= 0xFE20 && cp <= 0xFE2F) return true;
1305
+ // Cyrillic combining marks (0483-0489)
1306
+ if (cp >= 0x0483 && cp <= 0x0489) return true;
1307
+ // Hebrew combining marks (0591-05BD, 05BF, 05C1-05C2, 05C4-05C5, 05C7)
1308
+ if (cp >= 0x0591 && cp <= 0x05BD) return true;
1309
+ if (cp === 0x05BF || cp === 0x05C1 || cp === 0x05C2 || cp === 0x05C4 || cp === 0x05C5 || cp === 0x05C7) return true;
1310
+ // Arabic combining marks (0610-061A, 064B-065F, 0670, 06D6-06DC, 06DF-06E4, 06E7-06E8, 06EA-06ED)
1311
+ if (cp >= 0x0610 && cp <= 0x061A) return true;
1312
+ if (cp >= 0x064B && cp <= 0x065F) return true;
1313
+ if (cp === 0x0670) return true;
1314
+ if (cp >= 0x06D6 && cp <= 0x06DC) return true;
1315
+ if (cp >= 0x06DF && cp <= 0x06E4) return true;
1316
+ if (cp >= 0x06E7 && cp <= 0x06E8) return true;
1317
+ if (cp >= 0x06EA && cp <= 0x06ED) return true;
1318
+ // Devanagari, Bengali, etc. combining marks (0900-0903, 093A-094F, 0951-0957, 0962-0963)
1319
+ if (cp >= 0x0900 && cp <= 0x0903) return true;
1320
+ if (cp >= 0x093A && cp <= 0x094F) return true;
1321
+ if (cp >= 0x0951 && cp <= 0x0957) return true;
1322
+ if (cp >= 0x0962 && cp <= 0x0963) return true;
1323
+ return false;
1324
+ }
1325
+
1326
+ /**
1327
+ * Check if a code point is a skin tone modifier.
1328
+ */
1329
+ function isSkinToneModifier(cp: number): boolean {
1330
+ return cp >= SKIN_TONE_START && cp <= SKIN_TONE_END;
1331
+ }
1332
+
1333
+ /**
1334
+ * Check if a code point is a variation selector (VS1-VS16 or VS17-VS256).
1335
+ */
1336
+ function isVariationSelector(cp: number): boolean {
1337
+ // VS1-VS16 (FE00-FE0F)
1338
+ if (cp >= 0xFE00 && cp <= 0xFE0F) return true;
1339
+ // VS17-VS256 (E0100-E01EF)
1340
+ if (cp >= 0xE0100 && cp <= 0xE01EF) return true;
1341
+ return false;
1342
+ }
1343
+
1344
+ /**
1345
+ * Check if a code point is a tag character.
1346
+ */
1347
+ function isTagCharacter(cp: number): boolean {
1348
+ return (cp >= TAG_START && cp <= TAG_END) || cp === CANCEL_TAG;
1349
+ }
1350
+
1351
+ /**
1352
+ * Check if a code point is an emoji presentation character.
1353
+ * Simplified check covering common emoji ranges.
1354
+ */
1355
+ function isEmojiLike(cp: number): boolean {
1356
+ // Common emoji ranges
1357
+ if (cp >= 0x1F600 && cp <= 0x1F64F) return true; // Emoticons
1358
+ if (cp >= 0x1F300 && cp <= 0x1F5FF) return true; // Misc Symbols and Pictographs
1359
+ if (cp >= 0x1F680 && cp <= 0x1F6FF) return true; // Transport and Map
1360
+ if (cp >= 0x1F700 && cp <= 0x1F77F) return true; // Alchemical Symbols
1361
+ if (cp >= 0x1F780 && cp <= 0x1F7FF) return true; // Geometric Shapes Extended
1362
+ if (cp >= 0x1F800 && cp <= 0x1F8FF) return true; // Supplemental Arrows-C
1363
+ if (cp >= 0x1F900 && cp <= 0x1F9FF) return true; // Supplemental Symbols and Pictographs
1364
+ if (cp >= 0x1FA00 && cp <= 0x1FA6F) return true; // Chess Symbols
1365
+ if (cp >= 0x1FA70 && cp <= 0x1FAFF) return true; // Symbols and Pictographs Extended-A
1366
+ if (cp >= 0x2600 && cp <= 0x26FF) return true; // Misc symbols
1367
+ if (cp >= 0x2700 && cp <= 0x27BF) return true; // Dingbats
1368
+ if (cp >= 0x231A && cp <= 0x23FF) return true; // Misc technical (includes clock faces)
1369
+ if (cp >= 0xFE00 && cp <= 0xFE0F) return true; // Variation selectors
1370
+ if (cp === 0x200D) return true; // ZWJ
1371
+ if (cp >= 0x2702 && cp <= 0x27B0) return true; // Dingbats
1372
+ if (cp === 0x2764) return true; // Heavy heart
1373
+ if (cp === 0x2049 || cp === 0x203C) return true; // Exclamation marks
1374
+ return false;
1375
+ }
1376
+
1377
+ class SegmenterPolyfill {
1378
+ private _locale: string;
1379
+ private _granularity: string;
1380
+
1381
+ constructor(locales?: string | string[], options?: any) {
1382
+ const opts = options || {};
1383
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1384
+ this._granularity = opts.granularity || 'grapheme';
1385
+ if (!['grapheme', 'word', 'sentence'].includes(this._granularity)) {
1386
+ throw new RangeError(`Invalid granularity: ${this._granularity}`);
1387
+ }
1388
+ }
1389
+
1390
+ segment(input: string): SegmentsPolyfill {
1391
+ return new SegmentsPolyfill(String(input), this._granularity);
1392
+ }
1393
+
1394
+ resolvedOptions(): any {
1395
+ return {
1396
+ locale: this._locale,
1397
+ granularity: this._granularity,
1398
+ };
1399
+ }
1400
+
1401
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1402
+ if (!locales) return [];
1403
+ const list = Array.isArray(locales) ? locales : [locales];
1404
+ return list.filter((l) => typeof l === 'string');
1405
+ }
1406
+ }
1407
+
1408
+ class SegmentsPolyfill {
1409
+ private _input: string;
1410
+ private _granularity: string;
1411
+
1412
+ constructor(input: string, granularity: string) {
1413
+ this._input = input;
1414
+ this._granularity = granularity;
1415
+ }
1416
+
1417
+ containing(index: number): {segment: string; index: number; input: string; isWordLike?: boolean} | undefined {
1418
+ if (index < 0 || index >= this._input.length) return undefined;
1419
+ const segments = this._getSegments();
1420
+ for (const seg of segments) {
1421
+ if (index >= seg.index && index < seg.index + seg.segment.length) {
1422
+ return seg;
1423
+ }
1424
+ }
1425
+ return undefined;
1426
+ }
1427
+
1428
+ [Symbol.iterator](): Iterator<{segment: string; index: number; input: string; isWordLike?: boolean}> {
1429
+ const segments = this._getSegments();
1430
+ let i = 0;
1431
+ return {
1432
+ next(): IteratorResult<{segment: string; index: number; input: string; isWordLike?: boolean}> {
1433
+ if (i >= segments.length) return { value: undefined, done: true };
1434
+ return { value: segments[i++], done: false };
1435
+ },
1436
+ };
1437
+ }
1438
+
1439
+ private _getSegments(): Array<{segment: string; index: number; input: string; isWordLike?: boolean}> {
1440
+ const input = this._input;
1441
+ const results: Array<{segment: string; index: number; input: string; isWordLike?: boolean}> = [];
1442
+
1443
+ if (this._granularity === 'grapheme') {
1444
+ // Extended grapheme cluster segmentation
1445
+ // Handles: surrogate pairs, ZWJ sequences, regional indicators (flags),
1446
+ // variation selectors, skin tone modifiers, combining marks, tag sequences
1447
+ let i = 0;
1448
+ while (i < input.length) {
1449
+ const startIdx = i;
1450
+ const [cp, cpLen] = codePointAt(input, i);
1451
+ i += cpLen;
1452
+
1453
+ // Check if this is a regional indicator (flag emoji)
1454
+ if (isRegionalIndicator(cp)) {
1455
+ // Flags are pairs of regional indicators
1456
+ if (i < input.length) {
1457
+ const [nextCp, nextLen] = codePointAt(input, i);
1458
+ if (isRegionalIndicator(nextCp)) {
1459
+ i += nextLen;
1460
+ }
1461
+ }
1462
+ // After the flag, absorb any variation selectors
1463
+ while (i < input.length) {
1464
+ const [nextCp, nextLen] = codePointAt(input, i);
1465
+ if (isVariationSelector(nextCp)) {
1466
+ i += nextLen;
1467
+ } else {
1468
+ break;
1469
+ }
1470
+ }
1471
+ results.push({ segment: input.substring(startIdx, i), index: startIdx, input });
1472
+ continue;
1473
+ }
1474
+
1475
+ // Absorb combining marks, variation selectors, skin tone modifiers
1476
+ while (i < input.length) {
1477
+ const [nextCp, nextLen] = codePointAt(input, i);
1478
+
1479
+ // Combining marks
1480
+ if (isCombiningMark(nextCp)) {
1481
+ i += nextLen;
1482
+ continue;
1483
+ }
1484
+
1485
+ // Variation selectors (FE0E text, FE0F emoji)
1486
+ if (isVariationSelector(nextCp)) {
1487
+ i += nextLen;
1488
+ continue;
1489
+ }
1490
+
1491
+ // Skin tone modifiers
1492
+ if (isSkinToneModifier(nextCp)) {
1493
+ i += nextLen;
1494
+ continue;
1495
+ }
1496
+
1497
+ // Enclosing keycap
1498
+ if (nextCp === KEYCAP) {
1499
+ i += nextLen;
1500
+ continue;
1501
+ }
1502
+
1503
+ // Tag characters (for flag tag sequences like England flag)
1504
+ if (isTagCharacter(nextCp)) {
1505
+ i += nextLen;
1506
+ // Continue absorbing tags until cancel tag or non-tag
1507
+ while (i < input.length) {
1508
+ const [tagCp, tagLen] = codePointAt(input, i);
1509
+ if (isTagCharacter(tagCp)) {
1510
+ i += tagLen;
1511
+ if (tagCp === CANCEL_TAG) break;
1512
+ } else {
1513
+ break;
1514
+ }
1515
+ }
1516
+ continue;
1517
+ }
1518
+
1519
+ // ZWJ: merge with the next character/sequence
1520
+ if (nextCp === ZWJ) {
1521
+ i += nextLen;
1522
+ // Consume the next code point after ZWJ
1523
+ if (i < input.length) {
1524
+ const [joinedCp, joinedLen] = codePointAt(input, i);
1525
+ i += joinedLen;
1526
+ // Continue the loop to absorb more modifiers/ZWJ after the joined char
1527
+ }
1528
+ continue;
1529
+ }
1530
+
1531
+ break;
1532
+ }
1533
+
1534
+ // Handle \r\n as a single grapheme
1535
+ if (cp === 0x0D && i < input.length && input.charCodeAt(i) === 0x0A) {
1536
+ // \r was consumed, \n follows
1537
+ // Actually, \r is at startIdx, need to re-check
1538
+ }
1539
+
1540
+ results.push({ segment: input.substring(startIdx, i), index: startIdx, input });
1541
+ }
1542
+ } else if (this._granularity === 'word') {
1543
+ let match: RegExpExecArray | null;
1544
+ const regex = new RegExp(WORD_REGEX.source, 'g');
1545
+ while ((match = regex.exec(input)) !== null) {
1546
+ const segment = match[0];
1547
+ const isWordLike = !/^\s+$/.test(segment);
1548
+ results.push({ segment, index: match.index, input, isWordLike });
1549
+ }
1550
+ } else if (this._granularity === 'sentence') {
1551
+ if (input.length === 0) return results;
1552
+ let match: RegExpExecArray | null;
1553
+ const regex = new RegExp(SENTENCE_REGEX.source, 'g');
1554
+ while ((match = regex.exec(input)) !== null) {
1555
+ results.push({ segment: match[0], index: match.index, input });
1556
+ }
1557
+ // If regex didn't match anything (no sentence-ending punctuation), treat whole string as one sentence
1558
+ if (results.length === 0) {
1559
+ results.push({ segment: input, index: 0, input });
1560
+ }
1561
+ }
1562
+
1563
+ return results;
1564
+ }
1565
+ }
1566
+
1567
+ Object.defineProperty(Intl, 'Segmenter', {
1568
+ value: SegmenterPolyfill,
1569
+ writable: true,
1570
+ enumerable: false,
1571
+ configurable: true,
1572
+ });
1573
+ }
1574
+
1575
+ // --------------------------------------------------------------------------
1576
+ // Intl.DisplayNames
1577
+ // Provides display names for languages, regions, scripts, currencies.
1578
+ // Lightweight stub with a small set of common English names.
1579
+ // --------------------------------------------------------------------------
1580
+ if (typeof Intl.DisplayNames !== 'function') {
1581
+ const LANGUAGE_NAMES: Record<string, string> = {
1582
+ en: 'English', es: 'Spanish', fr: 'French', de: 'German', it: 'Italian',
1583
+ pt: 'Portuguese', ja: 'Japanese', ko: 'Korean', zh: 'Chinese',
1584
+ ar: 'Arabic', hi: 'Hindi', ru: 'Russian', nl: 'Dutch', sv: 'Swedish',
1585
+ pl: 'Polish', tr: 'Turkish', th: 'Thai', vi: 'Vietnamese',
1586
+ };
1587
+
1588
+ const REGION_NAMES: Record<string, string> = {
1589
+ US: 'United States', GB: 'United Kingdom', CA: 'Canada', AU: 'Australia',
1590
+ DE: 'Germany', FR: 'France', JP: 'Japan', CN: 'China', IN: 'India',
1591
+ BR: 'Brazil', MX: 'Mexico', ES: 'Spain', IT: 'Italy', KR: 'South Korea',
1592
+ RU: 'Russia', NL: 'Netherlands', SE: 'Sweden', NO: 'Norway', DK: 'Denmark',
1593
+ };
1594
+
1595
+ const CURRENCY_NAMES: Record<string, string> = {
1596
+ USD: 'US Dollar', EUR: 'Euro', GBP: 'British Pound', JPY: 'Japanese Yen',
1597
+ CNY: 'Chinese Yuan', KRW: 'South Korean Won', INR: 'Indian Rupee',
1598
+ BRL: 'Brazilian Real', CAD: 'Canadian Dollar', AUD: 'Australian Dollar',
1599
+ CHF: 'Swiss Franc', MXN: 'Mexican Peso', SEK: 'Swedish Krona',
1600
+ };
1601
+
1602
+ class DisplayNamesPolyfill {
1603
+ private _locale: string;
1604
+ private _type: string;
1605
+ private _style: string;
1606
+ private _fallback: string;
1607
+
1608
+ constructor(locales?: string | string[], options?: any) {
1609
+ const opts = options || {};
1610
+ if (!opts.type) {
1611
+ throw new TypeError('DisplayNames requires a "type" option');
1612
+ }
1613
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1614
+ this._type = opts.type;
1615
+ this._style = opts.style || 'long';
1616
+ this._fallback = opts.fallback || 'code';
1617
+
1618
+ if (!['language', 'region', 'script', 'currency', 'calendar', 'dateTimeField'].includes(this._type)) {
1619
+ throw new RangeError(`Invalid type: ${this._type}`);
1620
+ }
1621
+ }
1622
+
1623
+ of(code: string): string | undefined {
1624
+ if (typeof code !== 'string' || code.length === 0) {
1625
+ throw new RangeError('Invalid code for DisplayNames');
1626
+ }
1627
+
1628
+ let result: string | undefined;
1629
+
1630
+ switch (this._type) {
1631
+ case 'language':
1632
+ result = LANGUAGE_NAMES[code.toLowerCase()] || LANGUAGE_NAMES[code.split('-')[0].toLowerCase()];
1633
+ break;
1634
+ case 'region':
1635
+ result = REGION_NAMES[code.toUpperCase()];
1636
+ break;
1637
+ case 'currency':
1638
+ result = CURRENCY_NAMES[code.toUpperCase()];
1639
+ break;
1640
+ default:
1641
+ result = undefined;
1642
+ }
1643
+
1644
+ if (result === undefined) {
1645
+ if (this._fallback === 'code') return code;
1646
+ return undefined;
1647
+ }
1648
+
1649
+ return result;
1650
+ }
1651
+
1652
+ resolvedOptions(): any {
1653
+ return {
1654
+ locale: this._locale,
1655
+ type: this._type,
1656
+ style: this._style,
1657
+ fallback: this._fallback,
1658
+ };
1659
+ }
1660
+
1661
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1662
+ if (!locales) return [];
1663
+ const list = Array.isArray(locales) ? locales : [locales];
1664
+ return list.filter((l) => typeof l === 'string');
1665
+ }
1666
+ }
1667
+
1668
+ Object.defineProperty(Intl, 'DisplayNames', {
1669
+ value: DisplayNamesPolyfill,
1670
+ writable: true,
1671
+ enumerable: false,
1672
+ configurable: true,
1673
+ });
1674
+ }
1675
+
1676
+ // --------------------------------------------------------------------------
1677
+ // Intl.Collator
1678
+ // Fallback using String.prototype.localeCompare.
1679
+ // --------------------------------------------------------------------------
1680
+ if (typeof Intl.Collator !== 'function') {
1681
+ class CollatorPolyfill {
1682
+ private _locale: string;
1683
+ private _usage: string;
1684
+ private _sensitivity: string;
1685
+ private _ignorePunctuation: boolean;
1686
+ private _numeric: boolean;
1687
+ private _caseFirst: string;
1688
+ private _collation: string;
1689
+
1690
+ constructor(locales?: string | string[], options?: any) {
1691
+ const opts = options || {};
1692
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1693
+ this._usage = opts.usage || 'sort';
1694
+ this._sensitivity = opts.sensitivity || 'variant';
1695
+ this._ignorePunctuation = opts.ignorePunctuation || false;
1696
+ this._numeric = opts.numeric || false;
1697
+ this._caseFirst = opts.caseFirst || 'false';
1698
+ this._collation = opts.collation || 'default';
1699
+ }
1700
+
1701
+ compare(a: string, b: string): number {
1702
+ try {
1703
+ return String(a).localeCompare(String(b), this._locale, {
1704
+ sensitivity: this._sensitivity as any,
1705
+ ignorePunctuation: this._ignorePunctuation,
1706
+ numeric: this._numeric,
1707
+ caseFirst: this._caseFirst as any,
1708
+ });
1709
+ } catch (_e) {
1710
+ // If localeCompare doesn't support options, fall back to basic comparison
1711
+ return String(a).localeCompare(String(b));
1712
+ }
1713
+ }
1714
+
1715
+ resolvedOptions(): any {
1716
+ return {
1717
+ locale: this._locale,
1718
+ usage: this._usage,
1719
+ sensitivity: this._sensitivity,
1720
+ ignorePunctuation: this._ignorePunctuation,
1721
+ numeric: this._numeric,
1722
+ caseFirst: this._caseFirst,
1723
+ collation: this._collation,
1724
+ };
1725
+ }
1726
+
1727
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1728
+ if (!locales) return [];
1729
+ const list = Array.isArray(locales) ? locales : [locales];
1730
+ return list.filter((l) => typeof l === 'string');
1731
+ }
1732
+ }
1733
+
1734
+ Object.defineProperty(Intl, 'Collator', {
1735
+ value: CollatorPolyfill,
1736
+ writable: true,
1737
+ enumerable: false,
1738
+ configurable: true,
1739
+ });
1740
+ }
1741
+
1742
+ // --------------------------------------------------------------------------
1743
+ // Intl.DurationFormat
1744
+ // Formats durations (e.g., "1 hour, 2 minutes, 3 seconds" or "1:02:03").
1745
+ // Supports styles: long, short, narrow, digital.
1746
+ // --------------------------------------------------------------------------
1747
+ if (typeof Intl.DurationFormat !== 'function') {
1748
+ // Duration field definitions in display order
1749
+ const DURATION_FIELDS: Array<{
1750
+ field: string;
1751
+ long: [string, string];
1752
+ short: string;
1753
+ narrow: string;
1754
+ }> = [
1755
+ { field: 'years', long: ['year', 'years'], short: 'yr', narrow: 'y' },
1756
+ { field: 'months', long: ['month', 'months'], short: 'mo', narrow: 'mo' },
1757
+ { field: 'weeks', long: ['week', 'weeks'], short: 'wk', narrow: 'w' },
1758
+ { field: 'days', long: ['day', 'days'], short: 'day', narrow: 'd' },
1759
+ { field: 'hours', long: ['hour', 'hours'], short: 'hr', narrow: 'h' },
1760
+ { field: 'minutes', long: ['minute', 'minutes'], short: 'min', narrow: 'm' },
1761
+ { field: 'seconds', long: ['second', 'seconds'], short: 'sec', narrow: 's' },
1762
+ { field: 'milliseconds', long: ['millisecond', 'milliseconds'], short: 'ms', narrow: 'ms' },
1763
+ { field: 'microseconds', long: ['microsecond', 'microseconds'], short: '\u00B5s', narrow: '\u00B5s' },
1764
+ { field: 'nanoseconds', long: ['nanosecond', 'nanoseconds'], short: 'ns', narrow: 'ns' },
1765
+ ];
1766
+
1767
+ // Fields that belong to the time portion (used for digital style)
1768
+ const TIME_FIELDS = new Set(['hours', 'minutes', 'seconds']);
1769
+ const SUB_SECOND_FIELDS = new Set(['milliseconds', 'microseconds', 'nanoseconds']);
1770
+
1771
+ class DurationFormatPolyfill {
1772
+ _locale: string;
1773
+ _style: string;
1774
+
1775
+ constructor(locales?: string | string[], options?: any) {
1776
+ const opts = options || {};
1777
+ this._locale = Array.isArray(locales) ? (locales[0] || 'en') : (locales || 'en');
1778
+ this._style = opts.style || 'short';
1779
+ if (!['long', 'short', 'narrow', 'digital'].includes(this._style)) {
1780
+ throw new RangeError(`Invalid style: ${this._style}`);
1781
+ }
1782
+ }
1783
+
1784
+ format(duration: any): string {
1785
+ if (duration === null || duration === undefined || typeof duration !== 'object') {
1786
+ throw new TypeError('Duration must be an object');
1787
+ }
1788
+ const parts = this._buildParts(duration);
1789
+ return parts.map((p: any) => p.value).join('');
1790
+ }
1791
+
1792
+ formatToParts(duration: any): Array<{type: string; value: string}> {
1793
+ if (duration === null || duration === undefined || typeof duration !== 'object') {
1794
+ throw new TypeError('Duration must be an object');
1795
+ }
1796
+ return this._buildParts(duration);
1797
+ }
1798
+
1799
+ resolvedOptions(): any {
1800
+ return {
1801
+ locale: this._locale,
1802
+ style: this._style,
1803
+ numberingSystem: 'latn',
1804
+ };
1805
+ }
1806
+
1807
+ static supportedLocalesOf(locales?: string | string[]): string[] {
1808
+ if (!locales) return [];
1809
+ const list = Array.isArray(locales) ? locales : [locales];
1810
+ return list.filter((l) => typeof l === 'string');
1811
+ }
1812
+
1813
+ _buildParts(duration: any): Array<{type: string; value: string}> {
1814
+ if (this._style === 'digital') {
1815
+ return this._buildDigitalParts(duration);
1816
+ }
1817
+ return this._buildTextParts(duration);
1818
+ }
1819
+
1820
+ /**
1821
+ * Build parts for text styles: long, short, narrow.
1822
+ */
1823
+ _buildTextParts(duration: any): Array<{type: string; value: string}> {
1824
+ const parts: Array<{type: string; value: string}> = [];
1825
+ const segments: Array<{type: string; value: number; formatted: string}> = [];
1826
+
1827
+ for (const def of DURATION_FIELDS) {
1828
+ const val = Number(duration[def.field]) || 0;
1829
+ if (val === 0) continue;
1830
+
1831
+ let unitStr: string;
1832
+ if (this._style === 'long') {
1833
+ unitStr = val === 1 ? def.long[0] : def.long[1];
1834
+ } else if (this._style === 'short') {
1835
+ unitStr = def.short;
1836
+ } else {
1837
+ // narrow
1838
+ unitStr = def.narrow;
1839
+ }
1840
+
1841
+ let formatted: string;
1842
+ if (this._style === 'narrow') {
1843
+ formatted = `${val}${unitStr}`;
1844
+ } else {
1845
+ formatted = `${val} ${unitStr}`;
1846
+ }
1847
+
1848
+ segments.push({ type: def.field, value: val, formatted });
1849
+ }
1850
+
1851
+ for (let i = 0; i < segments.length; i++) {
1852
+ if (i > 0) {
1853
+ if (this._style === 'narrow') {
1854
+ parts.push({ type: 'literal', value: ' ' });
1855
+ } else {
1856
+ parts.push({ type: 'literal', value: ', ' });
1857
+ }
1858
+ }
1859
+
1860
+ const seg = segments[i];
1861
+ // Emit the integer part
1862
+ parts.push({ type: 'integer', value: String(seg.value) });
1863
+ // Emit the unit part
1864
+ if (this._style === 'narrow') {
1865
+ parts.push({ type: 'unit', value: seg.formatted.substring(String(seg.value).length) });
1866
+ } else {
1867
+ parts.push({ type: 'literal', value: ' ' });
1868
+ parts.push({ type: 'unit', value: seg.formatted.split(' ').slice(1).join(' ') });
1869
+ }
1870
+ }
1871
+
1872
+ return parts;
1873
+ }
1874
+
1875
+ /**
1876
+ * Build parts for digital style: "1:02:03"
1877
+ * Date fields (years, months, weeks, days) are formatted as text (short).
1878
+ * Time fields (hours, minutes, seconds) are formatted as HH:MM:SS.
1879
+ * Sub-second fields are appended after seconds with a decimal point.
1880
+ */
1881
+ _buildDigitalParts(duration: any): Array<{type: string; value: string}> {
1882
+ const parts: Array<{type: string; value: string}> = [];
1883
+
1884
+ // First, handle non-time fields (years, months, weeks, days) as short text
1885
+ const dateSegments: string[] = [];
1886
+ for (const def of DURATION_FIELDS) {
1887
+ if (TIME_FIELDS.has(def.field) || SUB_SECOND_FIELDS.has(def.field)) continue;
1888
+ const val = Number(duration[def.field]) || 0;
1889
+ if (val === 0) continue;
1890
+ dateSegments.push(`${val} ${def.short}`);
1891
+ }
1892
+
1893
+ if (dateSegments.length > 0) {
1894
+ parts.push({ type: 'literal', value: dateSegments.join(', ') });
1895
+ }
1896
+
1897
+ // Now handle time fields
1898
+ const hours = Number(duration.hours) || 0;
1899
+ const minutes = Number(duration.minutes) || 0;
1900
+ const seconds = Number(duration.seconds) || 0;
1901
+ const milliseconds = Number(duration.milliseconds) || 0;
1902
+ const microseconds = Number(duration.microseconds) || 0;
1903
+ const nanoseconds = Number(duration.nanoseconds) || 0;
1904
+
1905
+ const hasTimePart = hours > 0 || minutes > 0 || seconds > 0 ||
1906
+ milliseconds > 0 || microseconds > 0 || nanoseconds > 0;
1907
+
1908
+ if (!hasTimePart && parts.length > 0) {
1909
+ return parts;
1910
+ }
1911
+
1912
+ if (!hasTimePart && parts.length === 0) {
1913
+ // No fields at all - return "0:00:00"
1914
+ parts.push({ type: 'integer', value: '0' });
1915
+ parts.push({ type: 'literal', value: ':' });
1916
+ parts.push({ type: 'integer', value: '00' });
1917
+ parts.push({ type: 'literal', value: ':' });
1918
+ parts.push({ type: 'integer', value: '00' });
1919
+ return parts;
1920
+ }
1921
+
1922
+ if (dateSegments.length > 0) {
1923
+ parts.push({ type: 'literal', value: ', ' });
1924
+ }
1925
+
1926
+ // Format time as H:MM:SS
1927
+ const pad2 = (n: number): string => n < 10 ? `0${n}` : String(n);
1928
+
1929
+ parts.push({ type: 'integer', value: String(hours) });
1930
+ parts.push({ type: 'literal', value: ':' });
1931
+ parts.push({ type: 'integer', value: pad2(minutes) });
1932
+ parts.push({ type: 'literal', value: ':' });
1933
+ parts.push({ type: 'integer', value: pad2(seconds) });
1934
+
1935
+ // Sub-second precision
1936
+ const hasSubSecond = milliseconds > 0 || microseconds > 0 || nanoseconds > 0;
1937
+ if (hasSubSecond) {
1938
+ // Convert to fractional seconds string
1939
+ const totalNanos = milliseconds * 1_000_000 + microseconds * 1_000 + nanoseconds;
1940
+ const fracStr = String(totalNanos).padStart(9, '0').replace(/0+$/, '');
1941
+ parts.push({ type: 'literal', value: '.' });
1942
+ parts.push({ type: 'fraction', value: fracStr });
1943
+ }
1944
+
1945
+ return parts;
1946
+ }
1947
+ }
1948
+
1949
+ Object.defineProperty(Intl, 'DurationFormat', {
1950
+ value: DurationFormatPolyfill,
1951
+ writable: true,
1952
+ enumerable: false,
1953
+ configurable: true,
1954
+ });
1955
+ }
1956
+ }