@flighthq/intl 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.
@@ -0,0 +1,21 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Return the cached value for `key`, building and storing it with `build` on a
4
+ * miss. Memoizes the immutable `Intl.*` formatter instances the format
5
+ * functions share — building an `Intl.*` per call is the cost this package
6
+ * exists to remove.
7
+ *
8
+ * The cache is a bounded module-level `Map` (see the bottom of this file): it
9
+ * holds only immutable formatter instances, never mutable app state, and evicts
10
+ * the oldest entry once it exceeds `cacheCapacity` so it cannot grow unbounded.
11
+ */
12
+ export declare function getCached<T>(key: string, build: () => T): T;
13
+ /**
14
+ * Build the stable cache key for one formatter family. `kind` names the family
15
+ * (`'number'`, `'date'`, …); `locale` and `options` are serialized so that two
16
+ * calls with equivalent arguments hit the same entry. Options key order follows
17
+ * insertion order, which is stable because each format function assembles its
18
+ * options object the same way every call.
19
+ */
20
+ export declare function getCacheKey(kind: string, locale: LocaleInput, options: Readonly<object> | undefined): string;
21
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAW3D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,CAG5G"}
package/dist/cache.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Return the cached value for `key`, building and storing it with `build` on a
3
+ * miss. Memoizes the immutable `Intl.*` formatter instances the format
4
+ * functions share — building an `Intl.*` per call is the cost this package
5
+ * exists to remove.
6
+ *
7
+ * The cache is a bounded module-level `Map` (see the bottom of this file): it
8
+ * holds only immutable formatter instances, never mutable app state, and evicts
9
+ * the oldest entry once it exceeds `cacheCapacity` so it cannot grow unbounded.
10
+ */
11
+ export function getCached(key, build) {
12
+ const existing = formatterCache.get(key);
13
+ if (existing !== undefined)
14
+ return existing;
15
+ const built = build();
16
+ if (formatterCache.size >= cacheCapacity) {
17
+ const oldest = formatterCache.keys().next().value;
18
+ if (oldest !== undefined)
19
+ formatterCache.delete(oldest);
20
+ }
21
+ formatterCache.set(key, built);
22
+ return built;
23
+ }
24
+ /**
25
+ * Build the stable cache key for one formatter family. `kind` names the family
26
+ * (`'number'`, `'date'`, …); `locale` and `options` are serialized so that two
27
+ * calls with equivalent arguments hit the same entry. Options key order follows
28
+ * insertion order, which is stable because each format function assembles its
29
+ * options object the same way every call.
30
+ */
31
+ export function getCacheKey(kind, locale, options) {
32
+ const localeKey = typeof locale === 'string' ? locale : locale.join(',');
33
+ return `${kind}|${localeKey}|${options === undefined ? '' : JSON.stringify(options)}`;
34
+ }
35
+ // Immutable `Intl.*` instances keyed by `kind|locale|options`. A Map preserves
36
+ // insertion order, so the first key is the oldest and drives simple FIFO
37
+ // eviction once the cache is full.
38
+ const formatterCache = new Map();
39
+ const cacheCapacity = 256;
40
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAI,GAAW,EAAE,KAAc;IACtD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAa,CAAC;IAEjD,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;IACtB,IAAI,cAAc,CAAC,IAAI,IAAI,aAAa,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QAClD,IAAI,MAAM,KAAK,SAAS;YAAE,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1D,CAAC;IACD,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,MAAmB,EAAE,OAAqC;IAClG,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzE,OAAO,GAAG,IAAI,IAAI,SAAS,IAAI,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,+EAA+E;AAC/E,yEAAyE;AACzE,mCAAmC;AACnC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAmB,CAAC;AAClD,MAAM,aAAa,GAAG,GAAG,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Compare strings `a` and `b` for locale-aware ordering in `locale`, returning
4
+ * a negative number when `a` sorts before `b`, `0` when they sort equally, and
5
+ * a positive number when `a` sorts after `b` — the comparator contract
6
+ * `Array.prototype.sort` expects. Pass `{ sensitivity, numeric, caseFirst }`
7
+ * options to tune collation (e.g. `{ numeric: true }` for `'file2' < 'file10'`).
8
+ */
9
+ export declare function compareStrings(a: string, b: string, locale: LocaleInput, options?: Readonly<Intl.CollatorOptions>): number;
10
+ /**
11
+ * Return a new array of `items` sorted by locale-aware collation in `locale`.
12
+ * The input array is not mutated. Uses the same `Intl.Collator` (and cache) as
13
+ * `compareStrings`, so `options` tune ordering identically.
14
+ */
15
+ export declare function sortStrings(items: readonly string[], locale: LocaleInput, options?: Readonly<Intl.CollatorOptions>): string[];
16
+ //# sourceMappingURL=collator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collator.d.ts","sourceRoot":"","sources":["../src/collator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GACvC,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GACvC,MAAM,EAAE,CAGV"}
@@ -0,0 +1,25 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Compare strings `a` and `b` for locale-aware ordering in `locale`, returning
4
+ * a negative number when `a` sorts before `b`, `0` when they sort equally, and
5
+ * a positive number when `a` sorts after `b` — the comparator contract
6
+ * `Array.prototype.sort` expects. Pass `{ sensitivity, numeric, caseFirst }`
7
+ * options to tune collation (e.g. `{ numeric: true }` for `'file2' < 'file10'`).
8
+ */
9
+ export function compareStrings(a, b, locale, options) {
10
+ return getCollator(locale, options).compare(a, b);
11
+ }
12
+ /**
13
+ * Return a new array of `items` sorted by locale-aware collation in `locale`.
14
+ * The input array is not mutated. Uses the same `Intl.Collator` (and cache) as
15
+ * `compareStrings`, so `options` tune ordering identically.
16
+ */
17
+ export function sortStrings(items, locale, options) {
18
+ const collator = getCollator(locale, options);
19
+ return items.slice().sort((a, b) => collator.compare(a, b));
20
+ }
21
+ function getCollator(locale, options) {
22
+ const key = getCacheKey('collator', locale, options);
23
+ return getCached(key, () => new Intl.Collator(locale, options));
24
+ }
25
+ //# sourceMappingURL=collator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collator.js","sourceRoot":"","sources":["../src/collator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,CAAS,EACT,CAAS,EACT,MAAmB,EACnB,OAAwC;IAExC,OAAO,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,KAAwB,EACxB,MAAmB,EACnB,OAAwC;IAExC,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB,EAAE,OAAmD;IAC3F,MAAM,GAAG,GAAG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACrD,OAAO,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;AACvF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Format the date portion of `date` (a `Date` or epoch-millisecond number) in
4
+ * `locale`. Default field set: numeric `year`, `month`, and `day` — the order
5
+ * and separators follow the locale (`en-US` → `'1/15/2020'`, `de-DE` →
6
+ * `'15.1.2020'`). A caller `options` object replaces the default field set.
7
+ * An invalid `Date` passes through as `Intl`'s own `'Invalid Date'`.
8
+ */
9
+ export declare function formatDate(date: Date | number, locale: LocaleInput, options?: Readonly<Intl.DateTimeFormatOptions>): string;
10
+ /**
11
+ * Format both the date and time of `date` (a `Date` or epoch-millisecond
12
+ * number) in `locale`. Default field set: numeric `year`/`month`/`day` plus
13
+ * numeric `hour`/`minute`. A caller `options` object replaces the default field
14
+ * set.
15
+ */
16
+ export declare function formatDateTime(date: Date | number, locale: LocaleInput, options?: Readonly<Intl.DateTimeFormatOptions>): string;
17
+ /**
18
+ * Format the time portion of `date` (a `Date` or epoch-millisecond number) in
19
+ * `locale`. Default field set: numeric `hour` and `minute` (the locale decides
20
+ * 12- vs 24-hour). A caller `options` object replaces the default field set.
21
+ */
22
+ export declare function formatTime(date: Date | number, locale: LocaleInput, options?: Readonly<Intl.DateTimeFormatOptions>): string;
23
+ //# sourceMappingURL=datetime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,EACnB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,GAC7C,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,IAAI,GAAG,MAAM,EACnB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,GAC7C,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,EACnB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,GAC7C,MAAM,CAER"}
@@ -0,0 +1,50 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Format the date portion of `date` (a `Date` or epoch-millisecond number) in
4
+ * `locale`. Default field set: numeric `year`, `month`, and `day` — the order
5
+ * and separators follow the locale (`en-US` → `'1/15/2020'`, `de-DE` →
6
+ * `'15.1.2020'`). A caller `options` object replaces the default field set.
7
+ * An invalid `Date` passes through as `Intl`'s own `'Invalid Date'`.
8
+ */
9
+ export function formatDate(date, locale, options) {
10
+ return formatDateValue(date, locale, options ?? defaultDateOptions);
11
+ }
12
+ /**
13
+ * Format both the date and time of `date` (a `Date` or epoch-millisecond
14
+ * number) in `locale`. Default field set: numeric `year`/`month`/`day` plus
15
+ * numeric `hour`/`minute`. A caller `options` object replaces the default field
16
+ * set.
17
+ */
18
+ export function formatDateTime(date, locale, options) {
19
+ return formatDateValue(date, locale, options ?? defaultDateTimeOptions);
20
+ }
21
+ /**
22
+ * Format the time portion of `date` (a `Date` or epoch-millisecond number) in
23
+ * `locale`. Default field set: numeric `hour` and `minute` (the locale decides
24
+ * 12- vs 24-hour). A caller `options` object replaces the default field set.
25
+ */
26
+ export function formatTime(date, locale, options) {
27
+ return formatDateValue(date, locale, options ?? defaultTimeOptions);
28
+ }
29
+ // `Intl.DateTimeFormat.format` throws `RangeError` on an invalid `Date`, unlike
30
+ // `Date.prototype.toLocale*`. Guarding here keeps the charter's degradation
31
+ // contract — an invalid date returns `'Invalid Date'`, matching the `'NaN'`
32
+ // pass-through of the number formatters rather than surfacing as a throw.
33
+ function formatDateValue(date, locale, options) {
34
+ const time = typeof date === 'number' ? date : date.getTime();
35
+ if (Number.isNaN(time))
36
+ return 'Invalid Date';
37
+ const key = getCacheKey('datetime', locale, options);
38
+ const formatter = getCached(key, () => new Intl.DateTimeFormat(locale, options));
39
+ return formatter.format(date);
40
+ }
41
+ const defaultDateOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };
42
+ const defaultTimeOptions = { hour: 'numeric', minute: 'numeric' };
43
+ const defaultDateTimeOptions = {
44
+ year: 'numeric',
45
+ month: 'numeric',
46
+ day: 'numeric',
47
+ hour: 'numeric',
48
+ minute: 'numeric',
49
+ };
50
+ //# sourceMappingURL=datetime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datetime.js","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,IAAmB,EACnB,MAAmB,EACnB,OAA8C;IAE9C,OAAO,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,kBAAkB,CAAC,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAmB,EACnB,MAAmB,EACnB,OAA8C;IAE9C,OAAO,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,sBAAsB,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,IAAmB,EACnB,MAAmB,EACnB,OAA8C;IAE9C,OAAO,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,kBAAkB,CAAC,CAAC;AACtE,CAAC;AAED,gFAAgF;AAChF,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,SAAS,eAAe,CACtB,IAAmB,EACnB,MAAmB,EACnB,OAA6C;IAE7C,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;IAC9D,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IAE9C,MAAM,GAAG,GAAG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;IACtG,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,kBAAkB,GAAyC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AACvH,MAAM,kBAAkB,GAAyC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACxG,MAAM,sBAAsB,GAAyC;IACnE,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,GAAG,EAAE,SAAS;IACd,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;CAClB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export * from './collator';
2
+ export * from './datetime';
3
+ export * from './list';
4
+ export * from './number';
5
+ export * from './plural';
6
+ export * from './relativeTime';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './collator';
2
+ export * from './datetime';
3
+ export * from './list';
4
+ export * from './number';
5
+ export * from './plural';
6
+ export * from './relativeTime';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC"}
package/dist/list.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Join `items` into one locale-aware list string in `locale` — for example
4
+ * `['a', 'b', 'c']` → `'a, b, and c'` in `en-US`. Defaults to a conjunction
5
+ * (`'and'`) list; pass `{ type: 'disjunction' }` for an `'or'` list or
6
+ * `{ style: 'short' }` / `{ style: 'narrow' }` for tighter joins.
7
+ */
8
+ export declare function formatList(items: readonly string[], locale: LocaleInput, options?: Readonly<Intl.ListFormatOptions>): string;
9
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,GACzC,MAAM,CAIR"}
package/dist/list.js ADDED
@@ -0,0 +1,13 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Join `items` into one locale-aware list string in `locale` — for example
4
+ * `['a', 'b', 'c']` → `'a, b, and c'` in `en-US`. Defaults to a conjunction
5
+ * (`'and'`) list; pass `{ type: 'disjunction' }` for an `'or'` list or
6
+ * `{ style: 'short' }` / `{ style: 'narrow' }` for tighter joins.
7
+ */
8
+ export function formatList(items, locale, options) {
9
+ const key = getCacheKey('list', locale, options);
10
+ const formatter = getCached(key, () => new Intl.ListFormat(locale, options));
11
+ return formatter.format(items);
12
+ }
13
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,KAAwB,EACxB,MAAmB,EACnB,OAA0C;IAE1C,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;IAClG,OAAO,SAAS,CAAC,MAAM,CAAC,KAAiB,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Format `value` as a compact number in `locale` (`notation: 'compact'`) — for
4
+ * example `1200000` → `'1.2M'` in `en-US`. Merges over any caller `options`.
5
+ */
6
+ export declare function formatCompactNumber(value: number, locale: LocaleInput, options?: Readonly<Intl.NumberFormatOptions>): string;
7
+ /**
8
+ * Format `value` as an amount in `currency` (ISO 4217 code, e.g. `'USD'`) in
9
+ * `locale` — for example `1234.5` → `'$1,234.50'` in `en-US`. Sets
10
+ * `style: 'currency'` and `currency`; a caller may override other options.
11
+ */
12
+ export declare function formatCurrency(value: number, currency: string, locale: LocaleInput, options?: Readonly<Intl.NumberFormatOptions>): string;
13
+ /**
14
+ * Format `value` as a plain number in `locale` — for example `1234.56` →
15
+ * `'1,234.56'` in `en-US`, `'1.234,56'` in `de-DE`. `NaN`/`Infinity` and other
16
+ * edge inputs pass through `Intl`'s own output unchanged.
17
+ */
18
+ export declare function formatNumber(value: number, locale: LocaleInput, options?: Readonly<Intl.NumberFormatOptions>): string;
19
+ /**
20
+ * Format `value` as a percentage in `locale` (`style: 'percent'`) — the value
21
+ * is a ratio, so `0.25` → `'25%'` in `en-US`. Merges over any caller `options`.
22
+ */
23
+ export declare function formatPercent(value: number, locale: LocaleInput, options?: Readonly<Intl.NumberFormatOptions>): string;
24
+ /**
25
+ * Format `value` in a measurement `unit` (a sanctioned CLDR unit identifier such
26
+ * as `'kilometer-per-hour'`) in `locale` (`style: 'unit'`) — for example
27
+ * `5` → `'5 km/h'` in `en-US`. Sets `style: 'unit'` and `unit`; a caller may
28
+ * override other options.
29
+ */
30
+ export declare function formatUnit(value: number, unit: string, locale: LocaleInput, options?: Readonly<Intl.NumberFormatOptions>): string;
31
+ //# sourceMappingURL=number.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number.d.ts","sourceRoot":"","sources":["../src/number.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAC3C,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAC3C,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAErH;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAC3C,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAC3C,MAAM,CAER"}
package/dist/number.js ADDED
@@ -0,0 +1,45 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Format `value` as a compact number in `locale` (`notation: 'compact'`) — for
4
+ * example `1200000` → `'1.2M'` in `en-US`. Merges over any caller `options`.
5
+ */
6
+ export function formatCompactNumber(value, locale, options) {
7
+ return getNumberFormat(locale, { notation: 'compact', ...options }).format(value);
8
+ }
9
+ /**
10
+ * Format `value` as an amount in `currency` (ISO 4217 code, e.g. `'USD'`) in
11
+ * `locale` — for example `1234.5` → `'$1,234.50'` in `en-US`. Sets
12
+ * `style: 'currency'` and `currency`; a caller may override other options.
13
+ */
14
+ export function formatCurrency(value, currency, locale, options) {
15
+ return getNumberFormat(locale, { style: 'currency', currency, ...options }).format(value);
16
+ }
17
+ /**
18
+ * Format `value` as a plain number in `locale` — for example `1234.56` →
19
+ * `'1,234.56'` in `en-US`, `'1.234,56'` in `de-DE`. `NaN`/`Infinity` and other
20
+ * edge inputs pass through `Intl`'s own output unchanged.
21
+ */
22
+ export function formatNumber(value, locale, options) {
23
+ return getNumberFormat(locale, options).format(value);
24
+ }
25
+ /**
26
+ * Format `value` as a percentage in `locale` (`style: 'percent'`) — the value
27
+ * is a ratio, so `0.25` → `'25%'` in `en-US`. Merges over any caller `options`.
28
+ */
29
+ export function formatPercent(value, locale, options) {
30
+ return getNumberFormat(locale, { style: 'percent', ...options }).format(value);
31
+ }
32
+ /**
33
+ * Format `value` in a measurement `unit` (a sanctioned CLDR unit identifier such
34
+ * as `'kilometer-per-hour'`) in `locale` (`style: 'unit'`) — for example
35
+ * `5` → `'5 km/h'` in `en-US`. Sets `style: 'unit'` and `unit`; a caller may
36
+ * override other options.
37
+ */
38
+ export function formatUnit(value, unit, locale, options) {
39
+ return getNumberFormat(locale, { style: 'unit', unit, ...options }).format(value);
40
+ }
41
+ function getNumberFormat(locale, options) {
42
+ const key = getCacheKey('number', locale, options);
43
+ return getCached(key, () => new Intl.NumberFormat(locale, options));
44
+ }
45
+ //# sourceMappingURL=number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number.js","sourceRoot":"","sources":["../src/number.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,MAAmB,EACnB,OAA4C;IAE5C,OAAO,eAAe,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAa,EACb,QAAgB,EAChB,MAAmB,EACnB,OAA4C;IAE5C,OAAO,eAAe,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,MAAmB,EAAE,OAA4C;IAC3G,OAAO,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAa,EACb,MAAmB,EACnB,OAA4C;IAE5C,OAAO,eAAe,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,IAAY,EACZ,MAAmB,EACnB,OAA4C;IAE5C,OAAO,eAAe,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,eAAe,CACtB,MAAmB,EACnB,OAAuD;IAEvD,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;AAC3F,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Select the ordinal plural category of `value` in `locale` (`type: 'ordinal'`)
4
+ * — which grammatical form a positional number (`1st`, `2nd`, …) takes. In
5
+ * `en-US`: `1` → `'one'`, `2` → `'two'`, `3` → `'few'`, `4` → `'other'`. Use it
6
+ * to pick the right ordinal suffix per locale.
7
+ */
8
+ export declare function selectOrdinalCategory(value: number, locale: LocaleInput, options?: Readonly<Intl.PluralRulesOptions>): Intl.LDMLPluralRule;
9
+ /**
10
+ * Select the cardinal plural category of `value` in `locale` (`type:
11
+ * 'cardinal'`) — which grammatical form a count takes. In `en-US`: `1` →
12
+ * `'one'`, everything else → `'other'`; other locales return `'zero'` / `'two'`
13
+ * / `'few'` / `'many'` where their grammar distinguishes them. Use it to pick
14
+ * the right message variant for a count.
15
+ */
16
+ export declare function selectPluralCategory(value: number, locale: LocaleInput, options?: Readonly<Intl.PluralRulesOptions>): Intl.LDMLPluralRule;
17
+ //# sourceMappingURL=plural.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plural.d.ts","sourceRoot":"","sources":["../src/plural.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAC1C,IAAI,CAAC,cAAc,CAErB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAC1C,IAAI,CAAC,cAAc,CAErB"}
package/dist/plural.js ADDED
@@ -0,0 +1,25 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Select the ordinal plural category of `value` in `locale` (`type: 'ordinal'`)
4
+ * — which grammatical form a positional number (`1st`, `2nd`, …) takes. In
5
+ * `en-US`: `1` → `'one'`, `2` → `'two'`, `3` → `'few'`, `4` → `'other'`. Use it
6
+ * to pick the right ordinal suffix per locale.
7
+ */
8
+ export function selectOrdinalCategory(value, locale, options) {
9
+ return getPluralRules(locale, { type: 'ordinal', ...options }).select(value);
10
+ }
11
+ /**
12
+ * Select the cardinal plural category of `value` in `locale` (`type:
13
+ * 'cardinal'`) — which grammatical form a count takes. In `en-US`: `1` →
14
+ * `'one'`, everything else → `'other'`; other locales return `'zero'` / `'two'`
15
+ * / `'few'` / `'many'` where their grammar distinguishes them. Use it to pick
16
+ * the right message variant for a count.
17
+ */
18
+ export function selectPluralCategory(value, locale, options) {
19
+ return getPluralRules(locale, { type: 'cardinal', ...options }).select(value);
20
+ }
21
+ function getPluralRules(locale, options) {
22
+ const key = getCacheKey('plural', locale, options);
23
+ return getCached(key, () => new Intl.PluralRules(locale, options));
24
+ }
25
+ //# sourceMappingURL=plural.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plural.js","sourceRoot":"","sources":["../src/plural.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAa,EACb,MAAmB,EACnB,OAA2C;IAE3C,OAAO,cAAc,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAa,EACb,MAAmB,EACnB,OAA2C;IAE3C,OAAO,cAAc,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,cAAc,CAAC,MAAmB,EAAE,OAA0C;IACrF,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { LocaleInput } from '@flighthq/types';
2
+ /**
3
+ * Format `value` of `unit` as a locale-aware relative-time phrase in `locale` —
4
+ * a positive value is in the future, a negative value in the past. For example
5
+ * `formatRelativeTime(2, 'day', 'en-US')` → `'in 2 days'` and `(-1, 'day')` →
6
+ * `'1 day ago'`. Defaults to `numeric: 'always'`; pass `{ numeric: 'auto' }` to
7
+ * allow words like `'tomorrow'` / `'yesterday'`.
8
+ */
9
+ export declare function formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, locale: LocaleInput, options?: Readonly<Intl.RelativeTimeFormatOptions>): string;
10
+ //# sourceMappingURL=relativeTime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relativeTime.d.ts","sourceRoot":"","sources":["../src/relativeTime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,CAAC,sBAAsB,EACjC,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,CAAC,GACjD,MAAM,CAIR"}
@@ -0,0 +1,14 @@
1
+ import { getCacheKey, getCached } from './cache';
2
+ /**
3
+ * Format `value` of `unit` as a locale-aware relative-time phrase in `locale` —
4
+ * a positive value is in the future, a negative value in the past. For example
5
+ * `formatRelativeTime(2, 'day', 'en-US')` → `'in 2 days'` and `(-1, 'day')` →
6
+ * `'1 day ago'`. Defaults to `numeric: 'always'`; pass `{ numeric: 'auto' }` to
7
+ * allow words like `'tomorrow'` / `'yesterday'`.
8
+ */
9
+ export function formatRelativeTime(value, unit, locale, options) {
10
+ const key = getCacheKey('relativetime', locale, options);
11
+ const formatter = getCached(key, () => new Intl.RelativeTimeFormat(locale, options));
12
+ return formatter.format(value, unit);
13
+ }
14
+ //# sourceMappingURL=relativeTime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relativeTime.js","sourceRoot":"","sources":["../src/relativeTime.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,IAAiC,EACjC,MAAmB,EACnB,OAAkD;IAElD,MAAM,GAAG,GAAG,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAA2B,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1G,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/intl",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Locale-aware formatting — number, currency, percent, unit, date/time, list, relative-time, plural/ordinal, and collation over cached Intl instances",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getCacheKey, getCached } from './cache';
4
+
5
+ describe('getCached', () => {
6
+ it('builds once per key and reuses the stored value', () => {
7
+ const build = vi.fn(() => ({}));
8
+ const first = getCached('cache-test|reuse', build);
9
+ const second = getCached('cache-test|reuse', build);
10
+ expect(build).toHaveBeenCalledTimes(1);
11
+ expect(second).toBe(first);
12
+ });
13
+
14
+ it('rebuilds for a different key', () => {
15
+ const build = vi.fn(() => ({}));
16
+ const a = getCached('cache-test|a', build);
17
+ const b = getCached('cache-test|b', build);
18
+ expect(build).toHaveBeenCalledTimes(2);
19
+ expect(b).not.toBe(a);
20
+ });
21
+ });
22
+
23
+ describe('getCacheKey', () => {
24
+ it('serializes a string locale and options into a stable key', () => {
25
+ expect(getCacheKey('number', 'en-US', { style: 'percent' })).toBe('number|en-US|{"style":"percent"}');
26
+ });
27
+
28
+ it('joins a locale fallback list', () => {
29
+ expect(getCacheKey('number', ['fr-CA', 'fr'], undefined)).toBe('number|fr-CA,fr|');
30
+ });
31
+
32
+ it('separates keys by kind and by options', () => {
33
+ expect(getCacheKey('number', 'en-US', undefined)).not.toBe(getCacheKey('date', 'en-US', undefined));
34
+ expect(getCacheKey('number', 'en-US', { style: 'percent' })).not.toBe(
35
+ getCacheKey('number', 'en-US', { style: 'currency' }),
36
+ );
37
+ });
38
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { compareStrings, sortStrings } from './collator';
4
+
5
+ describe('compareStrings', () => {
6
+ it('orders a before b in en-US', () => {
7
+ expect(compareStrings('a', 'b', 'en-US')).toBeLessThan(0);
8
+ expect(compareStrings('b', 'a', 'en-US')).toBeGreaterThan(0);
9
+ });
10
+ });
11
+
12
+ describe('sortStrings', () => {
13
+ it('returns a new sorted array in en-US', () => {
14
+ expect(sortStrings(['b', 'a', 'c'], 'en-US')).toEqual(['a', 'b', 'c']);
15
+ });
16
+
17
+ it('does not mutate the input array', () => {
18
+ const input = ['b', 'a', 'c'];
19
+ sortStrings(input, 'en-US');
20
+ expect(input).toEqual(['b', 'a', 'c']);
21
+ });
22
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { formatDate, formatDateTime, formatTime } from './datetime';
4
+
5
+ // A fixed instant (2020-01-15T13:05:00Z). Assertions pass an explicit
6
+ // `timeZone: 'UTC'` so they are deterministic regardless of the host time zone.
7
+ const instant = new Date(Date.UTC(2020, 0, 15, 13, 5, 0));
8
+
9
+ describe('formatDate', () => {
10
+ it('orders numeric fields per locale', () => {
11
+ const utc = { year: 'numeric', month: 'numeric', day: 'numeric', timeZone: 'UTC' } as const;
12
+ expect(formatDate(instant, 'en-US', utc)).toBe('1/15/2020');
13
+ expect(formatDate(instant, 'de-DE', utc)).toBe('15.1.2020');
14
+ });
15
+
16
+ it('renders a string with the default field set', () => {
17
+ expect(typeof formatDate(instant, 'en-US')).toBe('string');
18
+ });
19
+
20
+ it('passes an invalid Date through as Intl does', () => {
21
+ expect(formatDate(new Date(NaN), 'en-US')).toBe('Invalid Date');
22
+ });
23
+ });
24
+
25
+ describe('formatDateTime', () => {
26
+ it('includes both the date and the time', () => {
27
+ const utc = {
28
+ year: 'numeric',
29
+ month: 'numeric',
30
+ day: 'numeric',
31
+ hour: 'numeric',
32
+ minute: 'numeric',
33
+ timeZone: 'UTC',
34
+ } as const;
35
+ const result = formatDateTime(instant, 'en-US', utc);
36
+ expect(result).toContain('1/15/2020');
37
+ expect(result).toContain('1:05');
38
+ });
39
+ });
40
+
41
+ describe('formatTime', () => {
42
+ it('formats the time per locale clock convention', () => {
43
+ const utc = { hour: 'numeric', minute: 'numeric', timeZone: 'UTC' } as const;
44
+ expect(formatTime(instant, 'en-US', utc)).toBe('1:05 PM');
45
+ expect(formatTime(instant, 'de-DE', utc)).toBe('13:05');
46
+ });
47
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { formatList } from './list';
4
+
5
+ describe('formatList', () => {
6
+ it('joins items as an en-US conjunction list', () => {
7
+ expect(formatList(['a', 'b', 'c'], 'en-US')).toBe('a, b, and c');
8
+ });
9
+
10
+ it('joins items as a disjunction list when asked', () => {
11
+ expect(formatList(['a', 'b', 'c'], 'en-US', { type: 'disjunction' })).toBe('a, b, or c');
12
+ });
13
+ });
@@ -0,0 +1,62 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { formatCompactNumber, formatCurrency, formatNumber, formatPercent, formatUnit } from './number';
4
+
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe('formatCompactNumber', () => {
10
+ it('abbreviates large numbers in en-US', () => {
11
+ expect(formatCompactNumber(1_200_000, 'en-US')).toBe('1.2M');
12
+ });
13
+ });
14
+
15
+ describe('formatCurrency', () => {
16
+ it('formats USD amounts in en-US with the currency symbol and two fraction digits', () => {
17
+ expect(formatCurrency(1234.5, 'USD', 'en-US')).toBe('$1,234.50');
18
+ });
19
+ });
20
+
21
+ describe('formatNumber', () => {
22
+ it('groups and separates by locale', () => {
23
+ expect(formatNumber(1234.56, 'en-US')).toBe('1,234.56');
24
+ expect(formatNumber(1234.56, 'de-DE')).toBe('1.234,56');
25
+ });
26
+
27
+ it('passes NaN through as Intl does', () => {
28
+ expect(formatNumber(NaN, 'en-US')).toBe('NaN');
29
+ });
30
+
31
+ it('reuses one cached Intl.NumberFormat for identical (locale, options) and builds another for different options', () => {
32
+ // Preserve real construction so `.format` still works, while counting builds.
33
+ // A regular function (not an arrow) so vitest can invoke it under `new`.
34
+ const OriginalNumberFormat = Intl.NumberFormat;
35
+ const spy = vi.spyOn(Intl, 'NumberFormat').mockImplementation(function (
36
+ locales?: Intl.LocalesArgument,
37
+ options?: Intl.NumberFormatOptions,
38
+ ) {
39
+ return new OriginalNumberFormat(locales, options);
40
+ });
41
+
42
+ // A locale used only by this test, so the cache starts empty for it.
43
+ formatNumber(1, 'ja-JP');
44
+ formatNumber(2, 'ja-JP');
45
+ expect(spy).toHaveBeenCalledTimes(1);
46
+
47
+ formatNumber(3, 'ja-JP', { minimumFractionDigits: 2 });
48
+ expect(spy).toHaveBeenCalledTimes(2);
49
+ });
50
+ });
51
+
52
+ describe('formatPercent', () => {
53
+ it('scales a ratio to a percentage in en-US', () => {
54
+ expect(formatPercent(0.25, 'en-US')).toBe('25%');
55
+ });
56
+ });
57
+
58
+ describe('formatUnit', () => {
59
+ it('renders a compound unit in en-US', () => {
60
+ expect(formatUnit(5, 'kilometer-per-hour', 'en-US')).toContain('km/h');
61
+ });
62
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { selectOrdinalCategory, selectPluralCategory } from './plural';
4
+
5
+ describe('selectOrdinalCategory', () => {
6
+ it('distinguishes ordinal forms in en-US', () => {
7
+ expect(selectOrdinalCategory(1, 'en-US')).toBe('one');
8
+ expect(selectOrdinalCategory(2, 'en-US')).toBe('two');
9
+ expect(selectOrdinalCategory(3, 'en-US')).toBe('few');
10
+ expect(selectOrdinalCategory(4, 'en-US')).toBe('other');
11
+ });
12
+ });
13
+
14
+ describe('selectPluralCategory', () => {
15
+ it('distinguishes cardinal forms in en-US', () => {
16
+ expect(selectPluralCategory(1, 'en-US')).toBe('one');
17
+ expect(selectPluralCategory(2, 'en-US')).toBe('other');
18
+ });
19
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { formatRelativeTime } from './relativeTime';
4
+
5
+ describe('formatRelativeTime', () => {
6
+ it('phrases a future offset in en-US', () => {
7
+ expect(formatRelativeTime(2, 'day', 'en-US')).toBe('in 2 days');
8
+ });
9
+
10
+ it('phrases a past offset in en-US', () => {
11
+ expect(formatRelativeTime(-1, 'day', 'en-US')).toBe('1 day ago');
12
+ });
13
+ });