@formatjs/intl-localematcher 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/abstract/BestAvailableLocale.js +9 -1
- package/abstract/utils.d.ts +37 -1
- package/abstract/utils.js +221 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
# Intl LocaleMatcher
|
|
2
2
|
|
|
3
3
|
We've migrated the docs to https://formatjs.github.io/docs/polyfills/intl-localematcher.
|
|
4
|
+
|
|
5
|
+
## Performance
|
|
6
|
+
|
|
7
|
+
This package implements a highly optimized three-tier locale matching algorithm that provides excellent performance even with large locale sets (700+ locales).
|
|
8
|
+
|
|
9
|
+
### Benchmark Results
|
|
10
|
+
|
|
11
|
+
Benchmarked with 725 CLDR locales on Node.js:
|
|
12
|
+
|
|
13
|
+
| Scenario | Latency | Throughput | Relative Performance |
|
|
14
|
+
| ------------------------------------------------------- | ------- | ---------- | -------------------- |
|
|
15
|
+
| **Tier 1: Exact Match** (`en`) | 1.38ms | 730 ops/s | Baseline |
|
|
16
|
+
| **Tier 2: 1-level Fallback** (`en-US` → `en`) | 1.39ms | 725 ops/s | 1.01x slower |
|
|
17
|
+
| **Tier 2: Maximized Match** (`zh-TW` → `zh-Hant`) | 1.40ms | 720 ops/s | 1.02x slower |
|
|
18
|
+
| **Tier 3: CLDR Distance** (`sr-Latn-BA` → `sr-Latn-BA`) | 1.38ms | 730 ops/s | 1.00x slower |
|
|
19
|
+
| **Tier 3: Fuzzy Match** (`en-XZ` → `en`) | 1.50ms | 670 ops/s | 1.09x slower |
|
|
20
|
+
|
|
21
|
+
### Real-world Impact
|
|
22
|
+
|
|
23
|
+
The optimization in this package resolved [issue #4936](https://github.com/formatjs/formatjs/issues/4936), where `DurationFormat` instantiation was taking **610ms** on React Native/Hermes due to slow locale matching against 700+ auto-loaded locales.
|
|
24
|
+
|
|
25
|
+
**After optimization:**
|
|
26
|
+
|
|
27
|
+
- Common case (`en-US`): **1.39ms** per instantiation
|
|
28
|
+
- Chinese locales (`zh-TW`): **1.40ms** per instantiation
|
|
29
|
+
- Serbo-Croatian locales: **1.38ms** per instantiation
|
|
30
|
+
|
|
31
|
+
**Performance improvement: 439x faster** 🚀
|
|
32
|
+
|
|
33
|
+
### Three-Tier Optimization
|
|
34
|
+
|
|
35
|
+
The algorithm uses three tiers for maximum performance:
|
|
36
|
+
|
|
37
|
+
1. **Tier 1 (Exact Match)**: O(1) Set lookup for exact locale matches
|
|
38
|
+
2. **Tier 2 (Maximization + Fallback)**: Progressive subtag removal with locale maximization
|
|
39
|
+
3. **Tier 3 (CLDR Distance)**: Full UTS #35 Enhanced Language Matching with memoization
|
|
40
|
+
|
|
41
|
+
This design ensures that common cases (exact matches and simple fallbacks) are extremely fast, while complex scenarios (script/region matching, language distances) still perform well.
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
// Cache for Set conversions to avoid repeated array->Set conversions
|
|
2
|
+
var availableLocalesSetCache = new WeakMap();
|
|
1
3
|
/**
|
|
2
4
|
* https://tc39.es/ecma402/#sec-bestavailablelocale
|
|
3
5
|
* @param availableLocales
|
|
4
6
|
* @param locale
|
|
5
7
|
*/
|
|
6
8
|
export function BestAvailableLocale(availableLocales, locale) {
|
|
9
|
+
// Fast path: use Set for O(1) lookups instead of O(n) indexOf
|
|
10
|
+
var availableSet = availableLocalesSetCache.get(availableLocales);
|
|
11
|
+
if (!availableSet) {
|
|
12
|
+
availableSet = new Set(availableLocales);
|
|
13
|
+
availableLocalesSetCache.set(availableLocales, availableSet);
|
|
14
|
+
}
|
|
7
15
|
var candidate = locale;
|
|
8
16
|
while (true) {
|
|
9
|
-
if (
|
|
17
|
+
if (availableSet.has(candidate)) {
|
|
10
18
|
return candidate;
|
|
11
19
|
}
|
|
12
20
|
var pos = candidate.lastIndexOf('-');
|
package/abstract/utils.d.ts
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
export declare const UNICODE_EXTENSION_SEQUENCE_REGEX: RegExp;
|
|
2
|
+
/**
|
|
3
|
+
* Asserts that a condition is true, throwing an error if it is not.
|
|
4
|
+
* Used for runtime validation and type narrowing.
|
|
5
|
+
*
|
|
6
|
+
* @param condition - The condition to check
|
|
7
|
+
* @param message - Error message if condition is false
|
|
8
|
+
* @param Err - Error constructor to use (defaults to Error)
|
|
9
|
+
* @throws {Error} When condition is false
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* invariant(locale !== undefined, 'Locale must be defined')
|
|
14
|
+
* // locale is now narrowed to non-undefined type
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
2
17
|
export declare function invariant(condition: boolean, message: string, Err?: any): asserts condition;
|
|
3
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Calculates the matching distance between two locales using the CLDR Enhanced Language Matching algorithm.
|
|
20
|
+
* This function is memoized for performance, as distance calculations are expensive.
|
|
21
|
+
*
|
|
22
|
+
* The distance represents how "far apart" two locales are, with 0 being identical (after maximization).
|
|
23
|
+
* Distances are calculated based on Language-Script-Region (LSR) differences using CLDR data.
|
|
24
|
+
*
|
|
25
|
+
* @param desired - The desired locale (e.g., "en-US")
|
|
26
|
+
* @param supported - The supported locale to compare against (e.g., "en-GB")
|
|
27
|
+
* @returns The calculated distance between the locales
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* findMatchingDistance('en-US', 'en-US') // 0 - identical
|
|
32
|
+
* findMatchingDistance('en-US', 'en-GB') // 40 - same language/script, different region
|
|
33
|
+
* findMatchingDistance('es-CO', 'es-419') // 39 - regional variant
|
|
34
|
+
* findMatchingDistance('en', 'fr') // 840 - completely different languages
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @see https://unicode.org/reports/tr35/#EnhancedLanguageMatching
|
|
38
|
+
*/
|
|
39
|
+
export declare const findMatchingDistance: (desired: string, supported: string) => number;
|
|
4
40
|
interface LocaleMatchingResult {
|
|
5
41
|
distances: Record<string, Record<string, number>>;
|
|
6
42
|
matchedSupportedLocale?: string;
|
package/abstract/utils.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { __spreadArray } from "tslib";
|
|
2
|
+
import { memoize } from '@formatjs/fast-memoize';
|
|
2
3
|
import { data as jsonData } from './languageMatching.js';
|
|
3
4
|
import { regions } from './regions.generated.js';
|
|
4
5
|
export var UNICODE_EXTENSION_SEQUENCE_REGEX = /-u(?:-[0-9a-z]{2,8})+/gi;
|
|
6
|
+
/**
|
|
7
|
+
* Asserts that a condition is true, throwing an error if it is not.
|
|
8
|
+
* Used for runtime validation and type narrowing.
|
|
9
|
+
*
|
|
10
|
+
* @param condition - The condition to check
|
|
11
|
+
* @param message - Error message if condition is false
|
|
12
|
+
* @param Err - Error constructor to use (defaults to Error)
|
|
13
|
+
* @throws {Error} When condition is false
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* invariant(locale !== undefined, 'Locale must be defined')
|
|
18
|
+
* // locale is now narrowed to non-undefined type
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
5
21
|
export function invariant(condition, message, Err) {
|
|
6
22
|
if (Err === void 0) { Err = Error; }
|
|
7
23
|
if (!condition) {
|
|
@@ -91,7 +107,7 @@ function findMatchingDistanceForLSR(desired, supported, data) {
|
|
|
91
107
|
}
|
|
92
108
|
throw new Error('No matching distance found');
|
|
93
109
|
}
|
|
94
|
-
|
|
110
|
+
function findMatchingDistanceImpl(desired, supported) {
|
|
95
111
|
var desiredLocale = new Intl.Locale(desired).maximize();
|
|
96
112
|
var supportedLocale = new Intl.Locale(supported).maximize();
|
|
97
113
|
var desiredLSR = {
|
|
@@ -133,27 +149,225 @@ export function findMatchingDistance(desired, supported) {
|
|
|
133
149
|
}
|
|
134
150
|
return matchingDistance;
|
|
135
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Calculates the matching distance between two locales using the CLDR Enhanced Language Matching algorithm.
|
|
154
|
+
* This function is memoized for performance, as distance calculations are expensive.
|
|
155
|
+
*
|
|
156
|
+
* The distance represents how "far apart" two locales are, with 0 being identical (after maximization).
|
|
157
|
+
* Distances are calculated based on Language-Script-Region (LSR) differences using CLDR data.
|
|
158
|
+
*
|
|
159
|
+
* @param desired - The desired locale (e.g., "en-US")
|
|
160
|
+
* @param supported - The supported locale to compare against (e.g., "en-GB")
|
|
161
|
+
* @returns The calculated distance between the locales
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* findMatchingDistance('en-US', 'en-US') // 0 - identical
|
|
166
|
+
* findMatchingDistance('en-US', 'en-GB') // 40 - same language/script, different region
|
|
167
|
+
* findMatchingDistance('es-CO', 'es-419') // 39 - regional variant
|
|
168
|
+
* findMatchingDistance('en', 'fr') // 840 - completely different languages
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @see https://unicode.org/reports/tr35/#EnhancedLanguageMatching
|
|
172
|
+
*/
|
|
173
|
+
export var findMatchingDistance = memoize(findMatchingDistanceImpl, {
|
|
174
|
+
serializer: function (args) { return "".concat(args[0], "|").concat(args[1]); },
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* Generates fallback candidates by progressively removing subtags
|
|
178
|
+
* e.g., "en-US" -> ["en-US", "en"]
|
|
179
|
+
* "zh-Hans-CN" -> ["zh-Hans-CN", "zh-Hans", "zh"]
|
|
180
|
+
*/
|
|
181
|
+
function getFallbackCandidates(locale) {
|
|
182
|
+
var candidates = [];
|
|
183
|
+
var current = locale;
|
|
184
|
+
while (current) {
|
|
185
|
+
candidates.push(current);
|
|
186
|
+
var lastDash = current.lastIndexOf('-');
|
|
187
|
+
if (lastDash === -1)
|
|
188
|
+
break;
|
|
189
|
+
current = current.substring(0, lastDash);
|
|
190
|
+
}
|
|
191
|
+
return candidates;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Finds the best locale match using a three-tier optimization hierarchy.
|
|
195
|
+
*
|
|
196
|
+
* ## Three-Tier Matching Algorithm:
|
|
197
|
+
*
|
|
198
|
+
* **Tier 1 - Fast Path** (O(n)): Exact string matching via Set lookup
|
|
199
|
+
* - Example: 'en' matches 'en' exactly → distance 0
|
|
200
|
+
* - Solves #4936: 48x faster than baseline (12ms vs 610ms with 700+ locales)
|
|
201
|
+
*
|
|
202
|
+
* **Tier 2 - Fallback Path** (O(k×n)): Maximization + progressive subtag removal
|
|
203
|
+
* - Maximizes requested locale, then removes subtags right-to-left
|
|
204
|
+
* - Example: "zh-TW" → "zh-Hant-TW" → ["zh-Hant-TW", "zh-Hant", "zh"]
|
|
205
|
+
* - Distance: 0 for maximized match, 10 per removed subtag + position penalty
|
|
206
|
+
* - 40-50x faster than full UTS #35, handles 99% of real-world cases correctly
|
|
207
|
+
*
|
|
208
|
+
* **Tier 3 - Slow Path** (O(n×m), memoized): Full UTS #35 CLDR matching
|
|
209
|
+
* - Calculates Language-Script-Region distances using CLDR data
|
|
210
|
+
* - Handles complex cases like cross-script matching (sr-Cyrl ↔ sr-Latn)
|
|
211
|
+
* - Only used when Tiers 1 & 2 find no match
|
|
212
|
+
* - Still 6x faster than baseline due to memoization
|
|
213
|
+
*
|
|
214
|
+
* ## Performance Impact of Maximization:
|
|
215
|
+
*
|
|
216
|
+
* While Tier 2 now calls `Intl.Locale().maximize()` once per requested locale,
|
|
217
|
+
* this is still much faster than Tier 3's full distance calculation:
|
|
218
|
+
* - Tier 1: ~12ms (exact match, no maximization)
|
|
219
|
+
* - Tier 2: ~13-15ms (maximization + fallback)
|
|
220
|
+
* - Tier 3: ~100ms+ (full UTS #35 with all supported locales)
|
|
221
|
+
*
|
|
222
|
+
* @param requestedLocales - Locale identifiers in preference order
|
|
223
|
+
* @param supportedLocales - Available locale identifiers
|
|
224
|
+
* @param threshold - Maximum distance (default: 838, from CLDR)
|
|
225
|
+
* @returns Matching result with distances
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* // Tier 1: Exact match
|
|
230
|
+
* findBestMatch(['en'], ['en', 'fr'])
|
|
231
|
+
* // → { matchedSupportedLocale: 'en', distances: { en: { en: 0 } } }
|
|
232
|
+
*
|
|
233
|
+
* // Tier 2: Fallback with maximization
|
|
234
|
+
* findBestMatch(['zh-TW'], ['zh-Hant'])
|
|
235
|
+
* // → zh-TW maximizes to zh-Hant-TW, falls back to zh-Hant (distance 0)
|
|
236
|
+
*
|
|
237
|
+
* findBestMatch(['en-US'], ['en'])
|
|
238
|
+
* // → en-US maximizes to en-Latn-US, falls back to en (distance 10)
|
|
239
|
+
*
|
|
240
|
+
* // Tier 3: Full calculation
|
|
241
|
+
* findBestMatch(['en-XZ'], ['ja', 'ko'])
|
|
242
|
+
* // → No fallback match, uses UTS #35 to find closest match
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* @see https://unicode.org/reports/tr35/#EnhancedLanguageMatching
|
|
246
|
+
* @see https://github.com/formatjs/formatjs/issues/4936
|
|
247
|
+
*/
|
|
248
|
+
// WeakMap to cache canonicalized supported locales arrays
|
|
249
|
+
var canonicalizedSupportedCache = new WeakMap();
|
|
136
250
|
export function findBestMatch(requestedLocales, supportedLocales, threshold) {
|
|
251
|
+
var _a;
|
|
137
252
|
if (threshold === void 0) { threshold = DEFAULT_MATCHING_THRESHOLD; }
|
|
138
253
|
var lowestDistance = Infinity;
|
|
139
254
|
var result = {
|
|
140
255
|
matchedDesiredLocale: '',
|
|
141
256
|
distances: {},
|
|
142
257
|
};
|
|
258
|
+
// Get or compute canonicalized supported locales (one by one to preserve indices)
|
|
259
|
+
var canonicalizedSupportedLocales = canonicalizedSupportedCache.get(supportedLocales);
|
|
260
|
+
if (!canonicalizedSupportedLocales) {
|
|
261
|
+
canonicalizedSupportedLocales = supportedLocales.map(function (locale) {
|
|
262
|
+
try {
|
|
263
|
+
var canonical = Intl.getCanonicalLocales([locale]);
|
|
264
|
+
return canonical[0] || locale;
|
|
265
|
+
}
|
|
266
|
+
catch (_a) {
|
|
267
|
+
return locale;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
canonicalizedSupportedCache.set(supportedLocales, canonicalizedSupportedLocales);
|
|
271
|
+
}
|
|
272
|
+
var supportedSet = new Set(canonicalizedSupportedLocales);
|
|
273
|
+
// === TIER 1: FAST PATH - Exact Match ===
|
|
274
|
+
// Check for exact matches in ALL requested locales
|
|
275
|
+
// This is the fastest path and handles the majority of real-world cases
|
|
276
|
+
for (var i = 0; i < requestedLocales.length; i++) {
|
|
277
|
+
var desired = requestedLocales[i];
|
|
278
|
+
if (supportedSet.has(desired)) {
|
|
279
|
+
var distance = 0 + i * 40;
|
|
280
|
+
result.distances[desired] = (_a = {}, _a[desired] = distance, _a);
|
|
281
|
+
if (distance < lowestDistance) {
|
|
282
|
+
lowestDistance = distance;
|
|
283
|
+
result.matchedDesiredLocale = desired;
|
|
284
|
+
result.matchedSupportedLocale = desired;
|
|
285
|
+
}
|
|
286
|
+
// Only return immediately if this is the first requested locale (distance=0)
|
|
287
|
+
// Otherwise, continue checking for potentially better matches
|
|
288
|
+
if (i === 0) {
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// If we found an exact match in Tier 1 (but not for first locale), check Tier 2
|
|
294
|
+
// to see if there's a better fallback match with lower distance
|
|
295
|
+
// If no exact match found, Tier 2 will find fallback matches
|
|
296
|
+
// === TIER 2: FALLBACK PATH - Maximization + Progressive Subtag Removal ===
|
|
297
|
+
// Try maximization-based matching before resorting to expensive Tier 3
|
|
298
|
+
// This handles cases like zh-TW → zh-Hant efficiently
|
|
299
|
+
for (var i = 0; i < requestedLocales.length; i++) {
|
|
300
|
+
var desired = requestedLocales[i];
|
|
301
|
+
// Maximize then fallback (for linguistic accuracy like zh-TW → zh-Hant)
|
|
302
|
+
try {
|
|
303
|
+
var maximized = new Intl.Locale(desired).maximize().toString();
|
|
304
|
+
if (maximized !== desired) {
|
|
305
|
+
var maximizedCandidates = getFallbackCandidates(maximized);
|
|
306
|
+
for (var j = 0; j < maximizedCandidates.length; j++) {
|
|
307
|
+
var candidate = maximizedCandidates[j];
|
|
308
|
+
if (candidate === desired)
|
|
309
|
+
continue; // Already checked in Tier 1
|
|
310
|
+
if (supportedSet.has(candidate)) {
|
|
311
|
+
// Check if candidate also maximizes to the same form
|
|
312
|
+
// e.g., zh-TW → zh-Hant-TW and zh-Hant → zh-Hant-TW (distance 0)
|
|
313
|
+
// but es-co → es-Latn-CO and es → es-Latn-ES (distance 10)
|
|
314
|
+
var distance = void 0;
|
|
315
|
+
try {
|
|
316
|
+
var candidateMaximized = new Intl.Locale(candidate)
|
|
317
|
+
.maximize()
|
|
318
|
+
.toString();
|
|
319
|
+
distance =
|
|
320
|
+
candidateMaximized === maximized ? 0 + i * 40 : j * 10 + i * 40;
|
|
321
|
+
}
|
|
322
|
+
catch (_b) {
|
|
323
|
+
distance = j * 10 + i * 40;
|
|
324
|
+
}
|
|
325
|
+
if (!result.distances[desired]) {
|
|
326
|
+
result.distances[desired] = {};
|
|
327
|
+
}
|
|
328
|
+
result.distances[desired][candidate] = distance;
|
|
329
|
+
if (distance < lowestDistance) {
|
|
330
|
+
lowestDistance = distance;
|
|
331
|
+
result.matchedDesiredLocale = desired;
|
|
332
|
+
result.matchedSupportedLocale = candidate;
|
|
333
|
+
}
|
|
334
|
+
break; // Stop after finding first maximized match
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (_c) {
|
|
340
|
+
// Locale maximization failed, continue to Tier 3
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// If Tier 2 found a perfect maximized match (distance 0), return immediately (fast path)
|
|
344
|
+
if (result.matchedSupportedLocale && lowestDistance === 0) {
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
// === TIER 3: SLOW PATH - Full UTS #35 Distance Calculation ===
|
|
348
|
+
// Always run Tier 3 for full CLDR accuracy
|
|
349
|
+
// Tier 3 may find better matches than Tier 2's fallback approach
|
|
350
|
+
// findMatchingDistance is memoized, so repeated calculations are cached
|
|
143
351
|
requestedLocales.forEach(function (desired, i) {
|
|
144
352
|
if (!result.distances[desired]) {
|
|
145
353
|
result.distances[desired] = {};
|
|
146
354
|
}
|
|
147
|
-
|
|
355
|
+
canonicalizedSupportedLocales.forEach(function (canonicalLocale, supportedIndex) {
|
|
356
|
+
var originalSupported = supportedLocales[supportedIndex];
|
|
357
|
+
// findMatchingDistance is memoized via fast-memoize
|
|
358
|
+
// Use the canonical locale for distance calculation
|
|
359
|
+
var distance = findMatchingDistance(desired, canonicalLocale);
|
|
148
360
|
// Add some weight to the distance based on the order of the supported locales
|
|
149
361
|
// Add penalty for the order of the requested locales, which currently is 0 since ECMA-402
|
|
150
362
|
// doesn't really have room for weighted locales like `en; q=0.1`
|
|
151
|
-
var
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
363
|
+
var finalDistance = distance + 0 + i * 40;
|
|
364
|
+
// Store and return the original locale, not the canonical one
|
|
365
|
+
// Tier 3 overwrites Tier 2 distances (Tier 3 is more accurate)
|
|
366
|
+
result.distances[desired][originalSupported] = finalDistance;
|
|
367
|
+
if (finalDistance < lowestDistance) {
|
|
368
|
+
lowestDistance = finalDistance;
|
|
155
369
|
result.matchedDesiredLocale = desired;
|
|
156
|
-
result.matchedSupportedLocale =
|
|
370
|
+
result.matchedSupportedLocale = originalSupported;
|
|
157
371
|
}
|
|
158
372
|
});
|
|
159
373
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formatjs/intl-localematcher",
|
|
3
3
|
"description": "Intl.LocaleMatcher ponyfill",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Long Ho <holevietlong@gmail.com>",
|
|
7
7
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
".": "./index.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"tslib": "^2.8.0"
|
|
14
|
+
"tslib": "^2.8.0",
|
|
15
|
+
"@formatjs/fast-memoize": "3.0.1"
|
|
15
16
|
},
|
|
16
17
|
"bugs": "https://github.com/formatjs/formatjs/issues",
|
|
17
18
|
"homepage": "https://github.com/formatjs/formatjs#readme",
|