intl.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  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. // Segmenters for Unicode text processing (lazily initialized)
  9. let graphemeSegmenter: Intl.Segmenter | null = null
  10. let wordSegmenter: Intl.Segmenter | null = null
  11. export function getGraphemeSegmenter(): Intl.Segmenter {
  12. if (!graphemeSegmenter) {
  13. graphemeSegmenter = new Intl.Segmenter(undefined, {
  14. granularity: 'grapheme',
  15. })
  16. }
  17. return graphemeSegmenter
  18. }
  19. /**
  20. * Extract the first grapheme cluster from a string.
  21. * Returns '' for empty strings.
  22. */
  23. export function firstGrapheme(text: string): string {
  24. if (!text) return ''
  25. const segments = getGraphemeSegmenter().segment(text)
  26. const first = segments[Symbol.iterator]().next().value
  27. return first?.segment ?? ''
  28. }
  29. /**
  30. * Extract the last grapheme cluster from a string.
  31. * Returns '' for empty strings.
  32. */
  33. export function lastGrapheme(text: string): string {
  34. if (!text) return ''
  35. let last = ''
  36. for (const { segment } of getGraphemeSegmenter().segment(text)) {
  37. last = segment
  38. }
  39. return last
  40. }
  41. export function getWordSegmenter(): Intl.Segmenter {
  42. if (!wordSegmenter) {
  43. wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
  44. }
  45. return wordSegmenter
  46. }
  47. // RelativeTimeFormat cache (keyed by style:numeric)
  48. const rtfCache = new Map<string, Intl.RelativeTimeFormat>()
  49. export function getRelativeTimeFormat(
  50. style: 'long' | 'short' | 'narrow',
  51. numeric: 'always' | 'auto',
  52. ): Intl.RelativeTimeFormat {
  53. const key = `${style}:${numeric}`
  54. let rtf = rtfCache.get(key)
  55. if (!rtf) {
  56. rtf = new Intl.RelativeTimeFormat('en', { style, numeric })
  57. rtfCache.set(key, rtf)
  58. }
  59. return rtf
  60. }
  61. // Timezone is constant for the process lifetime
  62. let cachedTimeZone: string | null = null
  63. export function getTimeZone(): string {
  64. if (!cachedTimeZone) {
  65. cachedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
  66. }
  67. return cachedTimeZone
  68. }
  69. // System locale language subtag (e.g. 'en', 'ja') is constant for the process
  70. // lifetime. null = not yet computed; undefined = computed but unavailable (so
  71. // a stripped-ICU environment fails once instead of retrying on every call).
  72. let cachedSystemLocaleLanguage: string | undefined | null = null
  73. export function getSystemLocaleLanguage(): string | undefined {
  74. if (cachedSystemLocaleLanguage === null) {
  75. try {
  76. const locale = Intl.DateTimeFormat().resolvedOptions().locale
  77. cachedSystemLocaleLanguage = new Intl.Locale(locale).language
  78. } catch {
  79. cachedSystemLocaleLanguage = undefined
  80. }
  81. }
  82. return cachedSystemLocaleLanguage
  83. }