@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.
- package/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- 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
|
+
}
|