Skip to content

Commit 1d61d3f

Browse files
committed
feat: add Intl polyfill stub modules for CLI
Add minimal Intl API polyfill stubs to support internationalization features in the CLI. These stubs provide placeholder implementations for Intl.NumberFormat, Intl.DateTimeFormat, Intl.PluralRules, and other Intl APIs used by bundled tools.
1 parent 3e3edf1 commit 1d61d3f

File tree

12 files changed

+565
-0
lines changed

12 files changed

+565
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @fileoverview Base class for all Intl stub implementations.
3+
*/
4+
5+
/**
6+
* Base class for all Intl stub implementations.
7+
* Accepts any constructor arguments but ignores them.
8+
*/
9+
export class IntlBase {}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @fileoverview Intl.Collator stub - Simple ASCII string comparison.
3+
*
4+
* Real behavior:
5+
* - Compares strings according to locale-specific rules
6+
* - Example (Swedish): new Intl.Collator('sv').compare('z', 'ö') → -1 (ö comes after z)
7+
*
8+
* Stub behavior:
9+
* - Simple ASCII comparison (no locale awareness)
10+
* - Example: compare('z', 'ö') → 1 (z < ö in ASCII)
11+
* - Ignores all locale and sensitivity options
12+
*
13+
* Trade-off: ASCII comparison is sufficient for English-only CLI tools.
14+
*/
15+
16+
import { IntlBase } from './base.mts'
17+
18+
export class CollatorStub extends IntlBase {
19+
locale: string
20+
options: Intl.CollatorOptions
21+
22+
constructor(_locales?: string | string[], options?: Intl.CollatorOptions) {
23+
super()
24+
this.locale = 'en-US'
25+
this.options = options || {}
26+
}
27+
28+
compare(a: string, b: string): number {
29+
// Simple ASCII comparison (no locale rules).
30+
if (a < b) {
31+
return -1
32+
}
33+
if (a > b) {
34+
return 1
35+
}
36+
return 0
37+
}
38+
39+
resolvedOptions(): Intl.ResolvedCollatorOptions {
40+
return {
41+
caseFirst: 'false' as const,
42+
collation: 'default',
43+
ignorePunctuation: false,
44+
locale: 'en-US',
45+
numeric: false,
46+
sensitivity: 'variant',
47+
usage: 'sort',
48+
}
49+
}
50+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @fileoverview Intl.DateTimeFormat stub - Formats dates as ISO-8601 strings.
3+
*
4+
* Real behavior:
5+
* - Formats dates according to locale-specific rules
6+
* - Example: new Intl.DateTimeFormat('fr-FR').format(date) → "15/10/2025"
7+
*
8+
* Stub behavior:
9+
* - Always returns ISO-8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)
10+
* - Ignores all locale parameters
11+
* - Example: new Intl.DateTimeFormat('fr-FR').format(date) → "2025-10-15T00:00:00.000Z"
12+
*
13+
* Trade-off: ISO-8601 is universal and unambiguous, suitable for CLI logging.
14+
*/
15+
16+
import { IntlBase } from './base.mts'
17+
18+
export class DateTimeFormatStub extends IntlBase {
19+
locale: string
20+
options: Intl.DateTimeFormatOptions
21+
22+
constructor(
23+
_locales?: string | string[],
24+
options?: Intl.DateTimeFormatOptions,
25+
) {
26+
super()
27+
this.locale = 'en-US'
28+
this.options = options || {}
29+
}
30+
31+
format(date?: Date | number): string {
32+
// Return ISO-8601 format (universal, locale-independent).
33+
if (!(date instanceof Date)) {
34+
date = new Date(date || Date.now())
35+
}
36+
return date.toISOString()
37+
}
38+
39+
formatToParts(date?: Date | number): Intl.DateTimeFormatPart[] {
40+
// Return minimal parts array.
41+
return [{ type: 'literal', value: this.format(date) }]
42+
}
43+
44+
resolvedOptions(): Intl.ResolvedDateTimeFormatOptions {
45+
return {
46+
calendar: 'gregory',
47+
locale: 'en-US',
48+
numberingSystem: 'latn',
49+
timeZone: 'UTC',
50+
}
51+
}
52+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @fileoverview Intl.DisplayNames stub - Returns input code without translation.
3+
*
4+
* Real behavior:
5+
* - Translates language/region/currency codes to localized names
6+
* - Example: of('US') with type 'region' → "United States" (in English)
7+
*
8+
* Stub behavior:
9+
* - Returns the input code unchanged (no translation database)
10+
* - Example: of('US') → "US"
11+
* - Ignores all locale and type parameters
12+
*
13+
* Trade-off: Codes are understandable without translation (e.g., "en-US", "USD").
14+
*/
15+
16+
import { IntlBase } from './base.mts'
17+
18+
export class DisplayNamesStub extends IntlBase {
19+
locale: string
20+
type: string
21+
22+
constructor(_locales: string | string[], options: Intl.DisplayNamesOptions) {
23+
super()
24+
this.locale = 'en-US'
25+
this.type = options?.type || 'language'
26+
}
27+
28+
of(code: string): string | undefined {
29+
// Just return the code itself (no translation).
30+
return code
31+
}
32+
33+
resolvedOptions(): Intl.ResolvedDisplayNamesOptions {
34+
return {
35+
fallback: 'code',
36+
locale: 'en-US',
37+
style: 'long',
38+
type: this.type as Intl.DisplayNamesType,
39+
}
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @fileoverview Helper functions for Intl stub.
3+
*/
4+
5+
/**
6+
* Returns canonical locales (always returns ['en-US'] for stub).
7+
*/
8+
export function getCanonicalLocales(
9+
locales?: string | readonly string[],
10+
): string[] {
11+
if (Array.isArray(locales)) {
12+
return locales.length > 0 ? ['en-US'] : []
13+
}
14+
return locales ? ['en-US'] : []
15+
}
16+
17+
/**
18+
* Returns supported values for various Intl properties.
19+
*/
20+
export function supportedValuesOf(
21+
key:
22+
| 'calendar'
23+
| 'collation'
24+
| 'currency'
25+
| 'numberingSystem'
26+
| 'timeZone'
27+
| 'unit',
28+
): string[] {
29+
const values: Record<string, string[]> = {
30+
calendar: ['gregory'],
31+
collation: ['default'],
32+
currency: ['USD'],
33+
numberingSystem: ['latn'],
34+
timeZone: ['UTC'],
35+
unit: ['meter', 'second', 'byte'],
36+
}
37+
return values[key] || []
38+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @fileoverview Intl stub polyfill - Install all Intl stubs if Intl is missing.
3+
*
4+
* This module installs minimal Intl stubs when Node.js is built with --with-intl=none.
5+
* It provides basic internationalization functionality sufficient for CLI tools.
6+
*/
7+
8+
export { CollatorStub } from './collator.mts'
9+
export { DateTimeFormatStub } from './date-time-format.mts'
10+
export { DisplayNamesStub } from './display-names.mts'
11+
export { ListFormatStub } from './list-format.mts'
12+
export { LocaleStub } from './locale.mts'
13+
export { NumberFormatStub } from './number-format.mts'
14+
export { PluralRulesStub } from './plural-rules.mts'
15+
export { RelativeTimeFormatStub } from './relative-time-format.mts'
16+
export { SegmenterStub } from './segmenter.mts'
17+
18+
import { CollatorStub } from './collator.mts'
19+
import { DateTimeFormatStub } from './date-time-format.mts'
20+
import { DisplayNamesStub } from './display-names.mts'
21+
import { getCanonicalLocales, supportedValuesOf } from './helpers.mts'
22+
import { ListFormatStub } from './list-format.mts'
23+
import { LocaleStub } from './locale.mts'
24+
import { NumberFormatStub } from './number-format.mts'
25+
import { PluralRulesStub } from './plural-rules.mts'
26+
import { RelativeTimeFormatStub } from './relative-time-format.mts'
27+
import { SegmenterStub } from './segmenter.mts'
28+
29+
/**
30+
* Install Intl stubs globally if Intl is not defined.
31+
* This happens automatically when this module is imported.
32+
*/
33+
if (typeof globalThis.Intl === 'undefined') {
34+
// Create Intl global object with all stubs.
35+
;(globalThis as typeof globalThis & { Intl: typeof Intl }).Intl = {
36+
Collator: CollatorStub as unknown as typeof Intl.Collator,
37+
DateTimeFormat: DateTimeFormatStub as unknown as typeof Intl.DateTimeFormat,
38+
DisplayNames: DisplayNamesStub as unknown as typeof Intl.DisplayNames,
39+
ListFormat: ListFormatStub as unknown as typeof Intl.ListFormat,
40+
Locale: LocaleStub as unknown as typeof Intl.Locale,
41+
NumberFormat: NumberFormatStub as unknown as typeof Intl.NumberFormat,
42+
PluralRules: PluralRulesStub as unknown as typeof Intl.PluralRules,
43+
RelativeTimeFormat:
44+
RelativeTimeFormatStub as unknown as typeof Intl.RelativeTimeFormat,
45+
Segmenter: SegmenterStub as unknown as typeof Intl.Segmenter,
46+
47+
// Static methods.
48+
getCanonicalLocales,
49+
supportedValuesOf,
50+
}
51+
52+
// Make it non-configurable like the real Intl.
53+
Object.defineProperty(globalThis, 'Intl', {
54+
configurable: false,
55+
enumerable: false,
56+
value: (globalThis as typeof globalThis & { Intl: typeof Intl }).Intl,
57+
writable: false,
58+
})
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @fileoverview Intl.ListFormat stub - English comma-separated lists.
3+
*
4+
* Real behavior:
5+
* - Formats lists according to locale-specific rules
6+
* - Example (Japanese): format(['a', 'b', 'c']) → "a、b、c" (uses 、 separator)
7+
*
8+
* Stub behavior:
9+
* - English format with "and": "a, b, and c"
10+
* - Always uses conjunction style with Oxford comma
11+
* - Example: format(['a', 'b']) → "a and b"
12+
* - Ignores all locale and type parameters
13+
*
14+
* Trade-off: English list format is standard and readable for CLI output.
15+
*/
16+
17+
import { IntlBase } from './base.mts'
18+
19+
export class ListFormatStub extends IntlBase {
20+
locale: string
21+
type: string
22+
23+
constructor(_locales?: string | string[], options?: Intl.ListFormatOptions) {
24+
super()
25+
this.locale = 'en-US'
26+
this.type = options?.type || 'conjunction'
27+
}
28+
29+
format(list: string[]): string {
30+
if (!Array.isArray(list) || list.length === 0) {
31+
return ''
32+
}
33+
if (list.length === 1) {
34+
return String(list[0])
35+
}
36+
if (list.length === 2) {
37+
return `${list[0]} and ${list[1]}`
38+
}
39+
40+
// 3+ items: "a, b, and c"
41+
const last = list[list.length - 1]
42+
const rest = list.slice(0, -1).join(', ')
43+
return `${rest}, and ${last}`
44+
}
45+
46+
formatToParts(
47+
list: string[],
48+
): Array<{ type: 'element' | 'literal'; value: string }> {
49+
return [{ type: 'element', value: this.format(list) }]
50+
}
51+
52+
resolvedOptions(): Intl.ResolvedListFormatOptions {
53+
return {
54+
locale: 'en-US',
55+
style: 'long',
56+
type: this.type as 'conjunction' | 'disjunction' | 'unit',
57+
}
58+
}
59+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @fileoverview Intl.Locale stub - Simple locale representation.
3+
*
4+
* Real behavior:
5+
* - Parses and canonicalizes locale identifiers
6+
* - Provides properties like language, region, script, etc.
7+
* - Example: new Intl.Locale('en-GB') → {language: 'en', region: 'GB'}
8+
*
9+
* Stub behavior:
10+
* - Always returns 'en-US' as the base name
11+
* - Provides minimal properties with defaults
12+
* - Ignores all options
13+
*
14+
* Trade-off: Simple locale representation is sufficient for CLI tools.
15+
*/
16+
17+
import { IntlBase } from './base.mts'
18+
19+
export class LocaleStub extends IntlBase {
20+
baseName: string
21+
language: string
22+
23+
constructor(_tag?: string, _options?: Intl.LocaleOptions) {
24+
super()
25+
this.baseName = 'en-US'
26+
this.language = 'en'
27+
}
28+
29+
override toString(): string {
30+
return this.baseName
31+
}
32+
}

0 commit comments

Comments
 (0)