@refraction-ui/astro 0.17.0 → 0.17.2

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.
Files changed (41) hide show
  1. package/dist/accordion/accordion.ts +155 -0
  2. package/dist/accordion/index.ts +9 -1
  3. package/dist/analytics/analytics-manager.ts +7 -2
  4. package/dist/analytics/http-sink.ts +3 -1
  5. package/dist/analytics/types.ts +6 -0
  6. package/dist/astro-location-selector/LocationSelector.astro +56 -0
  7. package/dist/astro-location-selector/LocationSelector.stories.ts +15 -0
  8. package/dist/astro-location-selector/index.ts +1 -0
  9. package/dist/calendar/calendar.ts +11 -6
  10. package/dist/call-controls/call-controls.ts +2 -4
  11. package/dist/carousel/carousel.ts +45 -0
  12. package/dist/carousel/index.ts +9 -1
  13. package/dist/command-input/command-input.ts +5 -5
  14. package/dist/conversation/conversation.ts +9 -7
  15. package/dist/conversation/types.ts +2 -0
  16. package/dist/date-picker/date-picker.ts +6 -3
  17. package/dist/device-frame/device-frame.ts +2 -4
  18. package/dist/emoji-picker/emoji-picker.ts +2 -3
  19. package/dist/footer/footer.ts +4 -1
  20. package/dist/i18n/countries.ts +122 -0
  21. package/dist/i18n/detect-region.ts +243 -0
  22. package/dist/i18n/index.ts +47 -0
  23. package/dist/i18n/languages.ts +181 -0
  24. package/dist/i18n/locale-utils.ts +123 -0
  25. package/dist/i18n/voice-registry.ts +92 -0
  26. package/dist/index.ts +1 -0
  27. package/dist/location-selector/index.ts +1 -0
  28. package/dist/location-selector/location-selector.ts +60 -0
  29. package/dist/logger/telemetry-manager.ts +7 -4
  30. package/dist/logger/types.ts +4 -0
  31. package/dist/pagination/index.ts +14 -1
  32. package/dist/pagination/pagination.styles.ts +33 -0
  33. package/dist/pagination/pagination.ts +160 -0
  34. package/dist/presence-indicator/presence-indicator.ts +2 -4
  35. package/dist/reaction-bar/reaction-bar.ts +2 -3
  36. package/dist/slider/slider.ts +134 -2
  37. package/dist/status-indicator/status-indicator.ts +2 -4
  38. package/dist/thread-view/thread-view.ts +6 -7
  39. package/dist/toast/toast.ts +16 -5
  40. package/dist/voice-pill/voice-pill.ts +2 -4
  41. package/package.json +2 -1
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Country type and registry for i18n support.
3
+ */
4
+
5
+ export interface Country {
6
+ code: string
7
+ name: string
8
+ flag: string
9
+ defaultLanguages: string[]
10
+ timezone?: string
11
+ }
12
+
13
+ export const COUNTRIES: Record<string, Country> = {
14
+ // North America
15
+ 'US': { code: 'US', name: 'United States', flag: '🇺🇸', defaultLanguages: ['en'], timezone: 'America/New_York' },
16
+ 'CA': { code: 'CA', name: 'Canada', flag: '🇨🇦', defaultLanguages: ['en', 'fr'], timezone: 'America/Toronto' },
17
+ 'MX': { code: 'MX', name: 'Mexico', flag: '🇲🇽', defaultLanguages: ['es'], timezone: 'America/Mexico_City' },
18
+
19
+ // UK & Ireland
20
+ 'GB': { code: 'GB', name: 'United Kingdom', flag: '🇬🇧', defaultLanguages: ['en'], timezone: 'Europe/London' },
21
+ 'IE': { code: 'IE', name: 'Ireland', flag: '🇮🇪', defaultLanguages: ['en'], timezone: 'Europe/Dublin' },
22
+
23
+ // Oceania
24
+ 'AU': { code: 'AU', name: 'Australia', flag: '🇦🇺', defaultLanguages: ['en'], timezone: 'Australia/Sydney' },
25
+ 'NZ': { code: 'NZ', name: 'New Zealand', flag: '🇳🇿', defaultLanguages: ['en'], timezone: 'Pacific/Auckland' },
26
+
27
+ // South Asia
28
+ 'IN': { code: 'IN', name: 'India', flag: '🇮🇳', defaultLanguages: ['hi', 'en'], timezone: 'Asia/Kolkata' },
29
+ 'PK': { code: 'PK', name: 'Pakistan', flag: '🇵🇰', defaultLanguages: ['ur', 'en'], timezone: 'Asia/Karachi' },
30
+ 'BD': { code: 'BD', name: 'Bangladesh', flag: '🇧🇩', defaultLanguages: ['bn'], timezone: 'Asia/Dhaka' },
31
+ 'LK': { code: 'LK', name: 'Sri Lanka', flag: '🇱🇰', defaultLanguages: ['ta', 'en'], timezone: 'Asia/Colombo' },
32
+ 'NP': { code: 'NP', name: 'Nepal', flag: '🇳🇵', defaultLanguages: ['hi', 'en'], timezone: 'Asia/Kathmandu' },
33
+
34
+ // Western Europe
35
+ 'FR': { code: 'FR', name: 'France', flag: '🇫🇷', defaultLanguages: ['fr'], timezone: 'Europe/Paris' },
36
+ 'DE': { code: 'DE', name: 'Germany', flag: '🇩🇪', defaultLanguages: ['de'], timezone: 'Europe/Berlin' },
37
+ 'ES': { code: 'ES', name: 'Spain', flag: '🇪🇸', defaultLanguages: ['es'], timezone: 'Europe/Madrid' },
38
+ 'IT': { code: 'IT', name: 'Italy', flag: '🇮🇹', defaultLanguages: ['it'], timezone: 'Europe/Rome' },
39
+ 'PT': { code: 'PT', name: 'Portugal', flag: '🇵🇹', defaultLanguages: ['pt'], timezone: 'Europe/Lisbon' },
40
+ 'NL': { code: 'NL', name: 'Netherlands', flag: '🇳🇱', defaultLanguages: ['nl'], timezone: 'Europe/Amsterdam' },
41
+ 'BE': { code: 'BE', name: 'Belgium', flag: '🇧🇪', defaultLanguages: ['nl', 'fr', 'de'], timezone: 'Europe/Brussels' },
42
+ 'CH': { code: 'CH', name: 'Switzerland', flag: '🇨🇭', defaultLanguages: ['de', 'fr', 'it'], timezone: 'Europe/Zurich' },
43
+ 'AT': { code: 'AT', name: 'Austria', flag: '🇦🇹', defaultLanguages: ['de'], timezone: 'Europe/Vienna' },
44
+
45
+ // Nordic
46
+ 'SE': { code: 'SE', name: 'Sweden', flag: '🇸🇪', defaultLanguages: ['sv'], timezone: 'Europe/Stockholm' },
47
+ 'NO': { code: 'NO', name: 'Norway', flag: '🇳🇴', defaultLanguages: ['no'], timezone: 'Europe/Oslo' },
48
+ 'DK': { code: 'DK', name: 'Denmark', flag: '🇩🇰', defaultLanguages: ['da'], timezone: 'Europe/Copenhagen' },
49
+ 'FI': { code: 'FI', name: 'Finland', flag: '🇫🇮', defaultLanguages: ['fi'], timezone: 'Europe/Helsinki' },
50
+
51
+ // Eastern Europe
52
+ 'PL': { code: 'PL', name: 'Poland', flag: '🇵🇱', defaultLanguages: ['pl'], timezone: 'Europe/Warsaw' },
53
+ 'CZ': { code: 'CZ', name: 'Czech Republic', flag: '🇨🇿', defaultLanguages: ['cs'], timezone: 'Europe/Prague' },
54
+ 'RO': { code: 'RO', name: 'Romania', flag: '🇷🇴', defaultLanguages: ['ro'], timezone: 'Europe/Bucharest' },
55
+ 'HU': { code: 'HU', name: 'Hungary', flag: '🇭🇺', defaultLanguages: ['hu'], timezone: 'Europe/Budapest' },
56
+ 'BG': { code: 'BG', name: 'Bulgaria', flag: '🇧🇬', defaultLanguages: ['bg'], timezone: 'Europe/Sofia' },
57
+ 'HR': { code: 'HR', name: 'Croatia', flag: '🇭🇷', defaultLanguages: ['hr'], timezone: 'Europe/Zagreb' },
58
+ 'SK': { code: 'SK', name: 'Slovakia', flag: '🇸🇰', defaultLanguages: ['sk'], timezone: 'Europe/Bratislava' },
59
+ 'SI': { code: 'SI', name: 'Slovenia', flag: '🇸🇮', defaultLanguages: ['sl'], timezone: 'Europe/Ljubljana' },
60
+ 'RS': { code: 'RS', name: 'Serbia', flag: '🇷🇸', defaultLanguages: ['sr'], timezone: 'Europe/Belgrade' },
61
+ 'UA': { code: 'UA', name: 'Ukraine', flag: '🇺🇦', defaultLanguages: ['uk'], timezone: 'Europe/Kyiv' },
62
+ 'RU': { code: 'RU', name: 'Russia', flag: '🇷🇺', defaultLanguages: ['ru'], timezone: 'Europe/Moscow' },
63
+ 'TR': { code: 'TR', name: 'Turkey', flag: '🇹🇷', defaultLanguages: ['tr'], timezone: 'Europe/Istanbul' },
64
+ 'GR': { code: 'GR', name: 'Greece', flag: '🇬🇷', defaultLanguages: ['el'], timezone: 'Europe/Athens' },
65
+
66
+ // East Asia
67
+ 'JP': { code: 'JP', name: 'Japan', flag: '🇯🇵', defaultLanguages: ['ja'], timezone: 'Asia/Tokyo' },
68
+ 'KR': { code: 'KR', name: 'South Korea', flag: '🇰🇷', defaultLanguages: ['ko'], timezone: 'Asia/Seoul' },
69
+ 'CN': { code: 'CN', name: 'China', flag: '🇨🇳', defaultLanguages: ['zh'], timezone: 'Asia/Shanghai' },
70
+ 'TW': { code: 'TW', name: 'Taiwan', flag: '🇹🇼', defaultLanguages: ['zh'], timezone: 'Asia/Taipei' },
71
+
72
+ // Southeast Asia
73
+ 'TH': { code: 'TH', name: 'Thailand', flag: '🇹🇭', defaultLanguages: ['th'], timezone: 'Asia/Bangkok' },
74
+ 'VN': { code: 'VN', name: 'Vietnam', flag: '🇻🇳', defaultLanguages: ['vi'], timezone: 'Asia/Ho_Chi_Minh' },
75
+ 'ID': { code: 'ID', name: 'Indonesia', flag: '🇮🇩', defaultLanguages: ['id'], timezone: 'Asia/Jakarta' },
76
+ 'MY': { code: 'MY', name: 'Malaysia', flag: '🇲🇾', defaultLanguages: ['ms', 'en'], timezone: 'Asia/Kuala_Lumpur' },
77
+ 'PH': { code: 'PH', name: 'Philippines', flag: '🇵🇭', defaultLanguages: ['en'], timezone: 'Asia/Manila' },
78
+ 'SG': { code: 'SG', name: 'Singapore', flag: '🇸🇬', defaultLanguages: ['en', 'ms', 'zh'], timezone: 'Asia/Singapore' },
79
+
80
+ // South America
81
+ 'BR': { code: 'BR', name: 'Brazil', flag: '🇧🇷', defaultLanguages: ['pt'], timezone: 'America/Sao_Paulo' },
82
+ 'AR': { code: 'AR', name: 'Argentina', flag: '🇦🇷', defaultLanguages: ['es'], timezone: 'America/Argentina/Buenos_Aires' },
83
+ 'CO': { code: 'CO', name: 'Colombia', flag: '🇨🇴', defaultLanguages: ['es'], timezone: 'America/Bogota' },
84
+ 'CL': { code: 'CL', name: 'Chile', flag: '🇨🇱', defaultLanguages: ['es'], timezone: 'America/Santiago' },
85
+
86
+ // Middle East & North Africa
87
+ 'SA': { code: 'SA', name: 'Saudi Arabia', flag: '🇸🇦', defaultLanguages: ['ar'], timezone: 'Asia/Riyadh' },
88
+ 'AE': { code: 'AE', name: 'United Arab Emirates', flag: '🇦🇪', defaultLanguages: ['ar', 'en'], timezone: 'Asia/Dubai' },
89
+ 'IL': { code: 'IL', name: 'Israel', flag: '🇮🇱', defaultLanguages: ['he'], timezone: 'Asia/Jerusalem' },
90
+ 'EG': { code: 'EG', name: 'Egypt', flag: '🇪🇬', defaultLanguages: ['ar'], timezone: 'Africa/Cairo' },
91
+ }
92
+
93
+ /**
94
+ * Get a country by its code.
95
+ */
96
+ export function getCountry(code: string): Country | undefined {
97
+ return COUNTRIES[code]
98
+ }
99
+
100
+ /**
101
+ * Get the flag emoji for a country code.
102
+ * Returns empty string if the code is not found.
103
+ */
104
+ export function getCountryFlag(code: string): string {
105
+ return COUNTRIES[code]?.flag ?? ''
106
+ }
107
+
108
+ /**
109
+ * Get the display name for a country code.
110
+ * Returns empty string if the code is not found.
111
+ */
112
+ export function getCountryName(code: string): string {
113
+ return COUNTRIES[code]?.name ?? ''
114
+ }
115
+
116
+ /**
117
+ * Get default languages for a country.
118
+ * Returns empty array if the country is not found.
119
+ */
120
+ export function getDefaultLanguagesForCountry(code: string): string[] {
121
+ return COUNTRIES[code]?.defaultLanguages ?? []
122
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Region detection utilities.
3
+ * Detects user country and language from browser/environment signals.
4
+ */
5
+
6
+ /** Mapping of IANA timezone identifiers to ISO country codes */
7
+ export const TIMEZONE_TO_COUNTRY: Record<string, string> = {
8
+ // United States
9
+ 'America/New_York': 'US',
10
+ 'America/Chicago': 'US',
11
+ 'America/Denver': 'US',
12
+ 'America/Los_Angeles': 'US',
13
+ 'America/Phoenix': 'US',
14
+ 'America/Anchorage': 'US',
15
+ 'Pacific/Honolulu': 'US',
16
+ 'America/Detroit': 'US',
17
+ 'America/Indiana/Indianapolis': 'US',
18
+ 'America/Boise': 'US',
19
+
20
+ // Canada
21
+ 'America/Toronto': 'CA',
22
+ 'America/Vancouver': 'CA',
23
+ 'America/Edmonton': 'CA',
24
+ 'America/Winnipeg': 'CA',
25
+ 'America/Halifax': 'CA',
26
+ 'America/St_Johns': 'CA',
27
+
28
+ // Mexico
29
+ 'America/Mexico_City': 'MX',
30
+ 'America/Cancun': 'MX',
31
+ 'America/Tijuana': 'MX',
32
+
33
+ // UK & Ireland
34
+ 'Europe/London': 'GB',
35
+ 'Europe/Dublin': 'IE',
36
+
37
+ // Western Europe
38
+ 'Europe/Paris': 'FR',
39
+ 'Europe/Berlin': 'DE',
40
+ 'Europe/Madrid': 'ES',
41
+ 'Europe/Rome': 'IT',
42
+ 'Europe/Lisbon': 'PT',
43
+ 'Europe/Amsterdam': 'NL',
44
+ 'Europe/Brussels': 'BE',
45
+ 'Europe/Zurich': 'CH',
46
+ 'Europe/Vienna': 'AT',
47
+
48
+ // Nordic
49
+ 'Europe/Stockholm': 'SE',
50
+ 'Europe/Oslo': 'NO',
51
+ 'Europe/Copenhagen': 'DK',
52
+ 'Europe/Helsinki': 'FI',
53
+
54
+ // Eastern Europe
55
+ 'Europe/Warsaw': 'PL',
56
+ 'Europe/Prague': 'CZ',
57
+ 'Europe/Bucharest': 'RO',
58
+ 'Europe/Budapest': 'HU',
59
+ 'Europe/Sofia': 'BG',
60
+ 'Europe/Zagreb': 'HR',
61
+ 'Europe/Bratislava': 'SK',
62
+ 'Europe/Ljubljana': 'SI',
63
+ 'Europe/Belgrade': 'RS',
64
+ 'Europe/Kyiv': 'UA',
65
+ 'Europe/Moscow': 'RU',
66
+ 'Europe/Istanbul': 'TR',
67
+ 'Europe/Athens': 'GR',
68
+
69
+ // South Asia
70
+ 'Asia/Kolkata': 'IN',
71
+ 'Asia/Calcutta': 'IN',
72
+ 'Asia/Karachi': 'PK',
73
+ 'Asia/Dhaka': 'BD',
74
+ 'Asia/Colombo': 'LK',
75
+ 'Asia/Kathmandu': 'NP',
76
+
77
+ // East Asia
78
+ 'Asia/Tokyo': 'JP',
79
+ 'Asia/Seoul': 'KR',
80
+ 'Asia/Shanghai': 'CN',
81
+ 'Asia/Hong_Kong': 'CN',
82
+ 'Asia/Taipei': 'TW',
83
+
84
+ // Southeast Asia
85
+ 'Asia/Bangkok': 'TH',
86
+ 'Asia/Ho_Chi_Minh': 'VN',
87
+ 'Asia/Jakarta': 'ID',
88
+ 'Asia/Kuala_Lumpur': 'MY',
89
+ 'Asia/Manila': 'PH',
90
+ 'Asia/Singapore': 'SG',
91
+
92
+ // Oceania
93
+ 'Australia/Sydney': 'AU',
94
+ 'Australia/Melbourne': 'AU',
95
+ 'Australia/Brisbane': 'AU',
96
+ 'Australia/Perth': 'AU',
97
+ 'Australia/Adelaide': 'AU',
98
+ 'Pacific/Auckland': 'NZ',
99
+
100
+ // South America
101
+ 'America/Sao_Paulo': 'BR',
102
+ 'America/Rio_Branco': 'BR',
103
+ 'America/Argentina/Buenos_Aires': 'AR',
104
+ 'America/Bogota': 'CO',
105
+ 'America/Santiago': 'CL',
106
+
107
+ // Middle East
108
+ 'Asia/Riyadh': 'SA',
109
+ 'Asia/Dubai': 'AE',
110
+ 'Asia/Jerusalem': 'IL',
111
+ 'Africa/Cairo': 'EG',
112
+ }
113
+
114
+ /** Mapping of BCP-47 locale strings to ISO country codes */
115
+ export const LOCALE_TO_COUNTRY: Record<string, string> = {
116
+ 'en-US': 'US',
117
+ 'en-GB': 'GB',
118
+ 'en-AU': 'AU',
119
+ 'en-CA': 'CA',
120
+ 'en-NZ': 'NZ',
121
+ 'en-IE': 'IE',
122
+ 'en-IN': 'IN',
123
+ 'en-SG': 'SG',
124
+ 'en-PH': 'PH',
125
+ 'en-MY': 'MY',
126
+ 'en-ZA': 'ZA',
127
+ 'hi-IN': 'IN',
128
+ 'te-IN': 'IN',
129
+ 'ta-IN': 'IN',
130
+ 'ur-PK': 'PK',
131
+ 'pa-IN': 'IN',
132
+ 'bn-IN': 'IN',
133
+ 'bn-BD': 'BD',
134
+ 'gu-IN': 'IN',
135
+ 'mr-IN': 'IN',
136
+ 'kn-IN': 'IN',
137
+ 'ml-IN': 'IN',
138
+ 'fr-FR': 'FR',
139
+ 'fr-CA': 'CA',
140
+ 'fr-BE': 'BE',
141
+ 'fr-CH': 'CH',
142
+ 'es-ES': 'ES',
143
+ 'es-MX': 'MX',
144
+ 'es-AR': 'AR',
145
+ 'es-CO': 'CO',
146
+ 'es-CL': 'CL',
147
+ 'de-DE': 'DE',
148
+ 'de-AT': 'AT',
149
+ 'de-CH': 'CH',
150
+ 'it-IT': 'IT',
151
+ 'it-CH': 'CH',
152
+ 'pt-PT': 'PT',
153
+ 'pt-BR': 'BR',
154
+ 'nl-NL': 'NL',
155
+ 'nl-BE': 'BE',
156
+ 'sv-SE': 'SE',
157
+ 'da-DK': 'DK',
158
+ 'nb-NO': 'NO',
159
+ 'nn-NO': 'NO',
160
+ 'fi-FI': 'FI',
161
+ 'pl-PL': 'PL',
162
+ 'cs-CZ': 'CZ',
163
+ 'ro-RO': 'RO',
164
+ 'hu-HU': 'HU',
165
+ 'bg-BG': 'BG',
166
+ 'hr-HR': 'HR',
167
+ 'sk-SK': 'SK',
168
+ 'sl-SI': 'SI',
169
+ 'sr-RS': 'SR',
170
+ 'uk-UA': 'UA',
171
+ 'el-GR': 'GR',
172
+ 'ru-RU': 'RU',
173
+ 'ja-JP': 'JP',
174
+ 'ko-KR': 'KR',
175
+ 'zh-CN': 'CN',
176
+ 'zh-TW': 'TW',
177
+ 'zh-HK': 'CN',
178
+ 'ar-SA': 'SA',
179
+ 'ar-AE': 'AE',
180
+ 'ar-EG': 'EG',
181
+ 'he-IL': 'IL',
182
+ 'tr-TR': 'TR',
183
+ 'th-TH': 'TH',
184
+ 'vi-VN': 'VN',
185
+ 'id-ID': 'ID',
186
+ 'ms-MY': 'MY',
187
+ }
188
+
189
+ /**
190
+ * Detect the user's country from environment signals.
191
+ * Strategy: timezone mapping -> navigator.languages locale mapping -> fallback 'US'
192
+ */
193
+ export function detectCountry(): string {
194
+ // 1. Try timezone mapping
195
+ try {
196
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
197
+ if (tz && TIMEZONE_TO_COUNTRY[tz]) {
198
+ return TIMEZONE_TO_COUNTRY[tz]
199
+ }
200
+ } catch {
201
+ // Intl may not be available
202
+ }
203
+
204
+ // 2. Try navigator.languages locale mapping
205
+ try {
206
+ if (typeof navigator !== 'undefined') {
207
+ const languages = navigator.languages
208
+ if (languages && languages.length > 0) {
209
+ for (const locale of languages) {
210
+ if (LOCALE_TO_COUNTRY[locale]) {
211
+ return LOCALE_TO_COUNTRY[locale]
212
+ }
213
+ }
214
+ }
215
+ }
216
+ } catch {
217
+ // navigator may not be available
218
+ }
219
+
220
+ // 3. Fallback
221
+ return 'US'
222
+ }
223
+
224
+ /**
225
+ * Detect the user's preferred language from navigator.languages.
226
+ * Returns the first language from navigator.languages, or 'en' as fallback.
227
+ */
228
+ export function detectLanguage(): string {
229
+ try {
230
+ if (typeof navigator !== 'undefined') {
231
+ const languages = (navigator as any).languages
232
+ if (languages && languages.length > 0) {
233
+ return languages[0]
234
+ }
235
+ if ((navigator as any).language) {
236
+ return (navigator as any).language
237
+ }
238
+ }
239
+ } catch {
240
+ // navigator may not be available
241
+ }
242
+ return 'en'
243
+ }
@@ -0,0 +1,47 @@
1
+ // Languages
2
+ export type { Language } from './languages.js'
3
+ export {
4
+ LANGUAGES,
5
+ getLanguage,
6
+ getSupportedLanguages,
7
+ getLanguagesByGroup,
8
+ getLanguageName,
9
+ isRTL,
10
+ getTtsLang,
11
+ } from './languages.js'
12
+
13
+ // Countries
14
+ export type { Country } from './countries.js'
15
+ export {
16
+ COUNTRIES,
17
+ getCountry,
18
+ getCountryFlag,
19
+ getCountryName,
20
+ getDefaultLanguagesForCountry,
21
+ } from './countries.js'
22
+
23
+ // Region detection
24
+ export {
25
+ TIMEZONE_TO_COUNTRY,
26
+ LOCALE_TO_COUNTRY,
27
+ detectCountry,
28
+ detectLanguage,
29
+ } from './detect-region.js'
30
+
31
+ // Voice registry
32
+ export type { VoiceOption } from './voice-registry.js'
33
+ export {
34
+ VOICES,
35
+ getVoicesForLanguage,
36
+ getBestVoice,
37
+ getVoicesByProvider,
38
+ } from './voice-registry.js'
39
+
40
+ // Locale utilities
41
+ export {
42
+ LOCALE_DISPLAY_NAMES,
43
+ getLocaleDisplayName,
44
+ getStoredLocale,
45
+ setStoredLocale,
46
+ getLocalizedValue,
47
+ } from './locale-utils.js'
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Language type and registry for i18n support.
3
+ */
4
+
5
+ export interface Language {
6
+ code: string
7
+ name: string
8
+ nativeName: string
9
+ direction: 'ltr' | 'rtl'
10
+ group?: string
11
+ }
12
+
13
+ export const LANGUAGES: Record<string, Language> = {
14
+ // English variants
15
+ 'en': { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', group: 'English' },
16
+ 'en-US': { code: 'en-US', name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', group: 'English' },
17
+ 'en-GB': { code: 'en-GB', name: 'English (UK)', nativeName: 'English (UK)', direction: 'ltr', group: 'English' },
18
+ 'en-IN': { code: 'en-IN', name: 'English (India)', nativeName: 'English (India)',direction: 'ltr', group: 'English' },
19
+
20
+ // Indian languages
21
+ 'hi': { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', direction: 'ltr', group: 'Indian' },
22
+ 'te': { code: 'te', name: 'Telugu', nativeName: 'తెలుగు', direction: 'ltr', group: 'Indian' },
23
+ 'ta': { code: 'ta', name: 'Tamil', nativeName: 'தமிழ்', direction: 'ltr', group: 'Indian' },
24
+ 'ur': { code: 'ur', name: 'Urdu', nativeName: 'اردو', direction: 'rtl', group: 'Indian' },
25
+ 'pa': { code: 'pa', name: 'Punjabi', nativeName: 'ਪੰਜਾਬੀ', direction: 'ltr', group: 'Indian' },
26
+ 'bn': { code: 'bn', name: 'Bengali', nativeName: 'বাংলা', direction: 'ltr', group: 'Indian' },
27
+ 'gu': { code: 'gu', name: 'Gujarati', nativeName: 'ગુજરાતી', direction: 'ltr', group: 'Indian' },
28
+ 'mr': { code: 'mr', name: 'Marathi', nativeName: 'मराठी', direction: 'ltr', group: 'Indian' },
29
+ 'kn': { code: 'kn', name: 'Kannada', nativeName: 'ಕನ್ನಡ', direction: 'ltr', group: 'Indian' },
30
+ 'ml': { code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം', direction: 'ltr', group: 'Indian' },
31
+
32
+ // Western European
33
+ 'fr': { code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr', group: 'European' },
34
+ 'es': { code: 'es', name: 'Spanish', nativeName: 'Español', direction: 'ltr', group: 'European' },
35
+ 'de': { code: 'de', name: 'German', nativeName: 'Deutsch', direction: 'ltr', group: 'European' },
36
+ 'it': { code: 'it', name: 'Italian', nativeName: 'Italiano', direction: 'ltr', group: 'European' },
37
+ 'pt': { code: 'pt', name: 'Portuguese', nativeName: 'Português', direction: 'ltr', group: 'European' },
38
+ 'pt-BR': { code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', direction: 'ltr', group: 'European' },
39
+ 'nl': { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', direction: 'ltr', group: 'European' },
40
+
41
+ // Nordic
42
+ 'sv': { code: 'sv', name: 'Swedish', nativeName: 'Svenska', direction: 'ltr', group: 'European' },
43
+ 'da': { code: 'da', name: 'Danish', nativeName: 'Dansk', direction: 'ltr', group: 'European' },
44
+ 'no': { code: 'no', name: 'Norwegian', nativeName: 'Norsk', direction: 'ltr', group: 'European' },
45
+ 'fi': { code: 'fi', name: 'Finnish', nativeName: 'Suomi', direction: 'ltr', group: 'European' },
46
+
47
+ // Eastern European
48
+ 'pl': { code: 'pl', name: 'Polish', nativeName: 'Polski', direction: 'ltr', group: 'European' },
49
+ 'cs': { code: 'cs', name: 'Czech', nativeName: 'Čeština', direction: 'ltr', group: 'European' },
50
+ 'ro': { code: 'ro', name: 'Romanian', nativeName: 'Română', direction: 'ltr', group: 'European' },
51
+ 'hu': { code: 'hu', name: 'Hungarian', nativeName: 'Magyar', direction: 'ltr', group: 'European' },
52
+ 'bg': { code: 'bg', name: 'Bulgarian', nativeName: 'Български', direction: 'ltr', group: 'European' },
53
+ 'hr': { code: 'hr', name: 'Croatian', nativeName: 'Hrvatski', direction: 'ltr', group: 'European' },
54
+ 'sk': { code: 'sk', name: 'Slovak', nativeName: 'Slovenčina', direction: 'ltr', group: 'European' },
55
+ 'sl': { code: 'sl', name: 'Slovenian', nativeName: 'Slovenščina', direction: 'ltr', group: 'European' },
56
+ 'sr': { code: 'sr', name: 'Serbian', nativeName: 'Srpski', direction: 'ltr', group: 'European' },
57
+ 'uk': { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', direction: 'ltr', group: 'European' },
58
+ 'el': { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', direction: 'ltr', group: 'European' },
59
+ 'ru': { code: 'ru', name: 'Russian', nativeName: 'Русский', direction: 'ltr', group: 'European' },
60
+
61
+ // East Asian
62
+ 'ja': { code: 'ja', name: 'Japanese', nativeName: '日本語', direction: 'ltr', group: 'East Asian' },
63
+ 'ko': { code: 'ko', name: 'Korean', nativeName: '한국어', direction: 'ltr', group: 'East Asian' },
64
+ 'zh': { code: 'zh', name: 'Chinese (Simplified)', nativeName: '中文(简体)', direction: 'ltr', group: 'East Asian' },
65
+ 'zh-TW': { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '中文(繁體)', direction: 'ltr', group: 'East Asian' },
66
+
67
+ // Middle Eastern
68
+ 'ar': { code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', group: 'Middle Eastern' },
69
+ 'he': { code: 'he', name: 'Hebrew', nativeName: 'עברית', direction: 'rtl', group: 'Middle Eastern' },
70
+ 'tr': { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', direction: 'ltr', group: 'Middle Eastern' },
71
+
72
+ // Southeast Asian
73
+ 'th': { code: 'th', name: 'Thai', nativeName: 'ไทย', direction: 'ltr', group: 'Southeast Asian' },
74
+ 'vi': { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', direction: 'ltr', group: 'Southeast Asian' },
75
+ 'id': { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', direction: 'ltr', group: 'Southeast Asian' },
76
+ 'ms': { code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu', direction: 'ltr', group: 'Southeast Asian' },
77
+ }
78
+
79
+ /** BCP-47 TTS language tag mappings for base language codes */
80
+ const TTS_LANG_MAP: Record<string, string> = {
81
+ 'en': 'en-US',
82
+ 'en-US': 'en-US',
83
+ 'en-GB': 'en-GB',
84
+ 'en-IN': 'en-IN',
85
+ 'hi': 'hi-IN',
86
+ 'te': 'te-IN',
87
+ 'ta': 'ta-IN',
88
+ 'ur': 'ur-PK',
89
+ 'pa': 'pa-IN',
90
+ 'bn': 'bn-IN',
91
+ 'gu': 'gu-IN',
92
+ 'mr': 'mr-IN',
93
+ 'kn': 'kn-IN',
94
+ 'ml': 'ml-IN',
95
+ 'fr': 'fr-FR',
96
+ 'es': 'es-ES',
97
+ 'de': 'de-DE',
98
+ 'it': 'it-IT',
99
+ 'pt': 'pt-PT',
100
+ 'pt-BR': 'pt-BR',
101
+ 'nl': 'nl-NL',
102
+ 'sv': 'sv-SE',
103
+ 'da': 'da-DK',
104
+ 'no': 'nb-NO',
105
+ 'fi': 'fi-FI',
106
+ 'pl': 'pl-PL',
107
+ 'cs': 'cs-CZ',
108
+ 'ro': 'ro-RO',
109
+ 'hu': 'hu-HU',
110
+ 'bg': 'bg-BG',
111
+ 'hr': 'hr-HR',
112
+ 'sk': 'sk-SK',
113
+ 'sl': 'sl-SI',
114
+ 'sr': 'sr-RS',
115
+ 'uk': 'uk-UA',
116
+ 'el': 'el-GR',
117
+ 'ru': 'ru-RU',
118
+ 'ja': 'ja-JP',
119
+ 'ko': 'ko-KR',
120
+ 'zh': 'zh-CN',
121
+ 'zh-TW': 'zh-TW',
122
+ 'ar': 'ar-SA',
123
+ 'he': 'he-IL',
124
+ 'tr': 'tr-TR',
125
+ 'th': 'th-TH',
126
+ 'vi': 'vi-VN',
127
+ 'id': 'id-ID',
128
+ 'ms': 'ms-MY',
129
+ }
130
+
131
+ /**
132
+ * Get a language by its code.
133
+ */
134
+ export function getLanguage(code: string): Language | undefined {
135
+ return LANGUAGES[code]
136
+ }
137
+
138
+ /**
139
+ * Get all supported languages as an array.
140
+ */
141
+ export function getSupportedLanguages(): Language[] {
142
+ return Object.values(LANGUAGES)
143
+ }
144
+
145
+ /**
146
+ * Get languages grouped by their language family/group.
147
+ */
148
+ export function getLanguagesByGroup(): Record<string, Language[]> {
149
+ const groups: Record<string, Language[]> = {}
150
+ for (const lang of Object.values(LANGUAGES)) {
151
+ const group = lang.group ?? 'Other'
152
+ if (!groups[group]) {
153
+ groups[group] = []
154
+ }
155
+ groups[group].push(lang)
156
+ }
157
+ return groups
158
+ }
159
+
160
+ /**
161
+ * Get the display name for a language code.
162
+ * Returns empty string if the code is not found.
163
+ */
164
+ export function getLanguageName(code: string): string {
165
+ return LANGUAGES[code]?.name ?? ''
166
+ }
167
+
168
+ /**
169
+ * Check if a language code represents a right-to-left language.
170
+ */
171
+ export function isRTL(code: string): boolean {
172
+ return LANGUAGES[code]?.direction === 'rtl'
173
+ }
174
+
175
+ /**
176
+ * Get the BCP-47 language tag suitable for TTS engines.
177
+ * Returns the code itself if no mapping exists.
178
+ */
179
+ export function getTtsLang(code: string): string {
180
+ return TTS_LANG_MAP[code] ?? code
181
+ }