@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.
- package/dist/accordion/accordion.ts +155 -0
- package/dist/accordion/index.ts +9 -1
- package/dist/analytics/analytics-manager.ts +7 -2
- package/dist/analytics/http-sink.ts +3 -1
- package/dist/analytics/types.ts +6 -0
- package/dist/astro-location-selector/LocationSelector.astro +56 -0
- package/dist/astro-location-selector/LocationSelector.stories.ts +15 -0
- package/dist/astro-location-selector/index.ts +1 -0
- package/dist/calendar/calendar.ts +11 -6
- package/dist/call-controls/call-controls.ts +2 -4
- package/dist/carousel/carousel.ts +45 -0
- package/dist/carousel/index.ts +9 -1
- package/dist/command-input/command-input.ts +5 -5
- package/dist/conversation/conversation.ts +9 -7
- package/dist/conversation/types.ts +2 -0
- package/dist/date-picker/date-picker.ts +6 -3
- package/dist/device-frame/device-frame.ts +2 -4
- package/dist/emoji-picker/emoji-picker.ts +2 -3
- package/dist/footer/footer.ts +4 -1
- package/dist/i18n/countries.ts +122 -0
- package/dist/i18n/detect-region.ts +243 -0
- package/dist/i18n/index.ts +47 -0
- package/dist/i18n/languages.ts +181 -0
- package/dist/i18n/locale-utils.ts +123 -0
- package/dist/i18n/voice-registry.ts +92 -0
- package/dist/index.ts +1 -0
- package/dist/location-selector/index.ts +1 -0
- package/dist/location-selector/location-selector.ts +60 -0
- package/dist/logger/telemetry-manager.ts +7 -4
- package/dist/logger/types.ts +4 -0
- package/dist/pagination/index.ts +14 -1
- package/dist/pagination/pagination.styles.ts +33 -0
- package/dist/pagination/pagination.ts +160 -0
- package/dist/presence-indicator/presence-indicator.ts +2 -4
- package/dist/reaction-bar/reaction-bar.ts +2 -3
- package/dist/slider/slider.ts +134 -2
- package/dist/status-indicator/status-indicator.ts +2 -4
- package/dist/thread-view/thread-view.ts +6 -7
- package/dist/toast/toast.ts +16 -5
- package/dist/voice-pill/voice-pill.ts +2 -4
- 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
|
+
}
|