95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
/**
|
|
* Shared Intl object instances with lazy initialization.
|
|
*
|
|
* Intl constructors are expensive (~0.05-0.1ms each), so we cache instances
|
|
* for reuse across the codebase instead of creating new ones each time.
|
|
* Lazy initialization ensures we only pay the cost when actually needed.
|
|
*/
|
|
|
|
// Segmenters for Unicode text processing (lazily initialized)
|
|
let graphemeSegmenter: Intl.Segmenter | null = null
|
|
let wordSegmenter: Intl.Segmenter | null = null
|
|
|
|
export function getGraphemeSegmenter(): Intl.Segmenter {
|
|
if (!graphemeSegmenter) {
|
|
graphemeSegmenter = new Intl.Segmenter(undefined, {
|
|
granularity: 'grapheme',
|
|
})
|
|
}
|
|
return graphemeSegmenter
|
|
}
|
|
|
|
/**
|
|
* Extract the first grapheme cluster from a string.
|
|
* Returns '' for empty strings.
|
|
*/
|
|
export function firstGrapheme(text: string): string {
|
|
if (!text) return ''
|
|
const segments = getGraphemeSegmenter().segment(text)
|
|
const first = segments[Symbol.iterator]().next().value
|
|
return first?.segment ?? ''
|
|
}
|
|
|
|
/**
|
|
* Extract the last grapheme cluster from a string.
|
|
* Returns '' for empty strings.
|
|
*/
|
|
export function lastGrapheme(text: string): string {
|
|
if (!text) return ''
|
|
let last = ''
|
|
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
|
last = segment
|
|
}
|
|
return last
|
|
}
|
|
|
|
export function getWordSegmenter(): Intl.Segmenter {
|
|
if (!wordSegmenter) {
|
|
wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
|
|
}
|
|
return wordSegmenter
|
|
}
|
|
|
|
// RelativeTimeFormat cache (keyed by style:numeric)
|
|
const rtfCache = new Map<string, Intl.RelativeTimeFormat>()
|
|
|
|
export function getRelativeTimeFormat(
|
|
style: 'long' | 'short' | 'narrow',
|
|
numeric: 'always' | 'auto',
|
|
): Intl.RelativeTimeFormat {
|
|
const key = `${style}:${numeric}`
|
|
let rtf = rtfCache.get(key)
|
|
if (!rtf) {
|
|
rtf = new Intl.RelativeTimeFormat('en', { style, numeric })
|
|
rtfCache.set(key, rtf)
|
|
}
|
|
return rtf
|
|
}
|
|
|
|
// Timezone is constant for the process lifetime
|
|
let cachedTimeZone: string | null = null
|
|
|
|
export function getTimeZone(): string {
|
|
if (!cachedTimeZone) {
|
|
cachedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
}
|
|
return cachedTimeZone
|
|
}
|
|
|
|
// System locale language subtag (e.g. 'en', 'ja') is constant for the process
|
|
// lifetime. null = not yet computed; undefined = computed but unavailable (so
|
|
// a stripped-ICU environment fails once instead of retrying on every call).
|
|
let cachedSystemLocaleLanguage: string | undefined | null = null
|
|
|
|
export function getSystemLocaleLanguage(): string | undefined {
|
|
if (cachedSystemLocaleLanguage === null) {
|
|
try {
|
|
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
|
cachedSystemLocaleLanguage = new Intl.Locale(locale).language
|
|
} catch {
|
|
cachedSystemLocaleLanguage = undefined
|
|
}
|
|
}
|
|
return cachedSystemLocaleLanguage
|
|
}
|