/ src / utils / intl.ts
intl.ts
 1  /**
 2   * Shared Intl object instances with lazy initialization.
 3   *
 4   * Intl constructors are expensive (~0.05-0.1ms each), so we cache instances
 5   * for reuse across the codebase instead of creating new ones each time.
 6   * Lazy initialization ensures we only pay the cost when actually needed.
 7   */
 8  
 9  // Segmenters for Unicode text processing (lazily initialized)
10  let graphemeSegmenter: Intl.Segmenter | null = null
11  let wordSegmenter: Intl.Segmenter | null = null
12  
13  export function getGraphemeSegmenter(): Intl.Segmenter {
14    if (!graphemeSegmenter) {
15      graphemeSegmenter = new Intl.Segmenter(undefined, {
16        granularity: 'grapheme',
17      })
18    }
19    return graphemeSegmenter
20  }
21  
22  /**
23   * Extract the first grapheme cluster from a string.
24   * Returns '' for empty strings.
25   */
26  export function firstGrapheme(text: string): string {
27    if (!text) return ''
28    const segments = getGraphemeSegmenter().segment(text)
29    const first = segments[Symbol.iterator]().next().value
30    return first?.segment ?? ''
31  }
32  
33  /**
34   * Extract the last grapheme cluster from a string.
35   * Returns '' for empty strings.
36   */
37  export function lastGrapheme(text: string): string {
38    if (!text) return ''
39    let last = ''
40    for (const { segment } of getGraphemeSegmenter().segment(text)) {
41      last = segment
42    }
43    return last
44  }
45  
46  export function getWordSegmenter(): Intl.Segmenter {
47    if (!wordSegmenter) {
48      wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
49    }
50    return wordSegmenter
51  }
52  
53  // RelativeTimeFormat cache (keyed by style:numeric)
54  const rtfCache = new Map<string, Intl.RelativeTimeFormat>()
55  
56  export function getRelativeTimeFormat(
57    style: 'long' | 'short' | 'narrow',
58    numeric: 'always' | 'auto',
59  ): Intl.RelativeTimeFormat {
60    const key = `${style}:${numeric}`
61    let rtf = rtfCache.get(key)
62    if (!rtf) {
63      rtf = new Intl.RelativeTimeFormat('en', { style, numeric })
64      rtfCache.set(key, rtf)
65    }
66    return rtf
67  }
68  
69  // Timezone is constant for the process lifetime
70  let cachedTimeZone: string | null = null
71  
72  export function getTimeZone(): string {
73    if (!cachedTimeZone) {
74      cachedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
75    }
76    return cachedTimeZone
77  }
78  
79  // System locale language subtag (e.g. 'en', 'ja') is constant for the process
80  // lifetime. null = not yet computed; undefined = computed but unavailable (so
81  // a stripped-ICU environment fails once instead of retrying on every call).
82  let cachedSystemLocaleLanguage: string | undefined | null = null
83  
84  export function getSystemLocaleLanguage(): string | undefined {
85    if (cachedSystemLocaleLanguage === null) {
86      try {
87        const locale = Intl.DateTimeFormat().resolvedOptions().locale
88        cachedSystemLocaleLanguage = new Intl.Locale(locale).language
89      } catch {
90        cachedSystemLocaleLanguage = undefined
91      }
92    }
93    return cachedSystemLocaleLanguage
94  }