@refraction-ui/astro 0.16.1 → 0.17.1
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/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/command-input/command-input.ts +5 -5
- package/dist/composer/emoji-trigger.ts +51 -0
- package/dist/composer/index.ts +1 -0
- package/dist/composer/styles.ts +64 -9
- package/dist/emoji-picker/emoji-data.ts +242 -248
- package/dist/emoji-picker/emoji-dataset.generated.ts +1943 -0
- package/dist/emoji-picker/emoji-picker.styles.ts +11 -3
- package/dist/emoji-picker/index.ts +20 -0
- package/dist/emoji-picker/stickers.ts +146 -0
- package/dist/emoji-picker/twemoji.ts +60 -0
- 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/rich-editor/emoji.ts +10 -116
- package/package.json +2 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale utilities for display names, storage, and localized value resolution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Display names for common locale codes (in their native language) */
|
|
6
|
+
export const LOCALE_DISPLAY_NAMES: Record<string, string> = {
|
|
7
|
+
'en': 'English',
|
|
8
|
+
'en-US': 'English (US)',
|
|
9
|
+
'en-GB': 'English (UK)',
|
|
10
|
+
'en-IN': 'English (India)',
|
|
11
|
+
'hi': 'हिन्दी',
|
|
12
|
+
'te': 'తెలుగు',
|
|
13
|
+
'ta': 'தமிழ்',
|
|
14
|
+
'ur': 'اردو',
|
|
15
|
+
'pa': 'ਪੰਜਾਬੀ',
|
|
16
|
+
'bn': 'বাংলা',
|
|
17
|
+
'gu': 'ગુજરાતી',
|
|
18
|
+
'mr': 'मराठी',
|
|
19
|
+
'kn': 'ಕನ್ನಡ',
|
|
20
|
+
'ml': 'മലയാളം',
|
|
21
|
+
'fr': 'Français',
|
|
22
|
+
'es': 'Español',
|
|
23
|
+
'de': 'Deutsch',
|
|
24
|
+
'it': 'Italiano',
|
|
25
|
+
'pt': 'Português',
|
|
26
|
+
'pt-BR': 'Português (Brasil)',
|
|
27
|
+
'nl': 'Nederlands',
|
|
28
|
+
'sv': 'Svenska',
|
|
29
|
+
'da': 'Dansk',
|
|
30
|
+
'no': 'Norsk',
|
|
31
|
+
'fi': 'Suomi',
|
|
32
|
+
'pl': 'Polski',
|
|
33
|
+
'cs': 'Čeština',
|
|
34
|
+
'ro': 'Română',
|
|
35
|
+
'hu': 'Magyar',
|
|
36
|
+
'bg': 'Български',
|
|
37
|
+
'hr': 'Hrvatski',
|
|
38
|
+
'sk': 'Slovenčina',
|
|
39
|
+
'sl': 'Slovenščina',
|
|
40
|
+
'sr': 'Srpski',
|
|
41
|
+
'uk': 'Українська',
|
|
42
|
+
'el': 'Ελληνικά',
|
|
43
|
+
'ru': 'Русский',
|
|
44
|
+
'ja': '日本語',
|
|
45
|
+
'ko': '한국어',
|
|
46
|
+
'zh': '中文',
|
|
47
|
+
'zh-TW': '中文(繁體)',
|
|
48
|
+
'ar': 'العربية',
|
|
49
|
+
'he': 'עברית',
|
|
50
|
+
'tr': 'Türkçe',
|
|
51
|
+
'th': 'ไทย',
|
|
52
|
+
'vi': 'Tiếng Việt',
|
|
53
|
+
'id': 'Bahasa Indonesia',
|
|
54
|
+
'ms': 'Bahasa Melayu',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_STORAGE_KEY = 'refraction-ui-locale'
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the display name for a locale code.
|
|
61
|
+
* Returns the code itself if not found in the display names map.
|
|
62
|
+
*/
|
|
63
|
+
export function getLocaleDisplayName(code: string): string {
|
|
64
|
+
return LOCALE_DISPLAY_NAMES[code] ?? code
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the stored locale from localStorage (or compatible storage).
|
|
69
|
+
* Returns null if not stored or localStorage is not available.
|
|
70
|
+
*/
|
|
71
|
+
export function getStoredLocale(storageKey?: string): string | null {
|
|
72
|
+
try {
|
|
73
|
+
if (typeof localStorage !== 'undefined') {
|
|
74
|
+
return localStorage.getItem(storageKey ?? DEFAULT_STORAGE_KEY)
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// localStorage may not be available
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Store a locale code in localStorage (or compatible storage).
|
|
84
|
+
* Does nothing if localStorage is not available.
|
|
85
|
+
*/
|
|
86
|
+
export function setStoredLocale(code: string, storageKey?: string): void {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof localStorage !== 'undefined') {
|
|
89
|
+
localStorage.setItem(storageKey ?? DEFAULT_STORAGE_KEY, code)
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// localStorage may not be available
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a localized value from a record of locale -> value.
|
|
98
|
+
* Tries exact locale, then base language code (for regional variants like 'en-US' -> 'en'),
|
|
99
|
+
* then the fallback locale.
|
|
100
|
+
*/
|
|
101
|
+
export function getLocalizedValue<T>(
|
|
102
|
+
localized: Record<string, T>,
|
|
103
|
+
locale: string,
|
|
104
|
+
fallbackLocale?: string,
|
|
105
|
+
): T | undefined {
|
|
106
|
+
// 1. Exact match
|
|
107
|
+
if (locale in localized) {
|
|
108
|
+
return localized[locale]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Try base language code (e.g., 'en-US' -> 'en')
|
|
112
|
+
const baseLang = locale.split('-')[0]
|
|
113
|
+
if (baseLang !== locale && baseLang in localized) {
|
|
114
|
+
return localized[baseLang]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. Fallback locale
|
|
118
|
+
if (fallbackLocale && fallbackLocale in localized) {
|
|
119
|
+
return localized[fallbackLocale]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice registry for TTS providers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface VoiceOption {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
language: string
|
|
9
|
+
provider: 'browser' | 'google' | 'elevenlabs' | 'openai'
|
|
10
|
+
gender?: 'male' | 'female'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VOICES: VoiceOption[] = [
|
|
14
|
+
// Browser voices — English
|
|
15
|
+
{ id: 'browser-en-us-female', name: 'English (US) Female', language: 'en-US', provider: 'browser', gender: 'female' },
|
|
16
|
+
{ id: 'browser-en-us-male', name: 'English (US) Male', language: 'en-US', provider: 'browser', gender: 'male' },
|
|
17
|
+
{ id: 'browser-en-gb-female', name: 'English (UK) Female', language: 'en-GB', provider: 'browser', gender: 'female' },
|
|
18
|
+
{ id: 'browser-en-gb-male', name: 'English (UK) Male', language: 'en-GB', provider: 'browser', gender: 'male' },
|
|
19
|
+
{ id: 'browser-en-in-female', name: 'English (India) Female', language: 'en-IN', provider: 'browser', gender: 'female' },
|
|
20
|
+
|
|
21
|
+
// Browser voices — European
|
|
22
|
+
{ id: 'browser-fr-female', name: 'French Female', language: 'fr', provider: 'browser', gender: 'female' },
|
|
23
|
+
{ id: 'browser-de-female', name: 'German Female', language: 'de', provider: 'browser', gender: 'female' },
|
|
24
|
+
{ id: 'browser-es-female', name: 'Spanish Female', language: 'es', provider: 'browser', gender: 'female' },
|
|
25
|
+
{ id: 'browser-it-female', name: 'Italian Female', language: 'it', provider: 'browser', gender: 'female' },
|
|
26
|
+
{ id: 'browser-pt-female', name: 'Portuguese Female', language: 'pt', provider: 'browser', gender: 'female' },
|
|
27
|
+
|
|
28
|
+
// Browser voices — Asian
|
|
29
|
+
{ id: 'browser-ja-female', name: 'Japanese Female', language: 'ja', provider: 'browser', gender: 'female' },
|
|
30
|
+
{ id: 'browser-ko-female', name: 'Korean Female', language: 'ko', provider: 'browser', gender: 'female' },
|
|
31
|
+
{ id: 'browser-zh-female', name: 'Chinese Female', language: 'zh', provider: 'browser', gender: 'female' },
|
|
32
|
+
{ id: 'browser-hi-female', name: 'Hindi Female', language: 'hi', provider: 'browser', gender: 'female' },
|
|
33
|
+
{ id: 'browser-ar-male', name: 'Arabic Male', language: 'ar', provider: 'browser', gender: 'male' },
|
|
34
|
+
|
|
35
|
+
// Google TTS voices
|
|
36
|
+
{ id: 'google-en-us-wavenet-d', name: 'Google WaveNet English (US)', language: 'en-US', provider: 'google', gender: 'male' },
|
|
37
|
+
{ id: 'google-en-us-wavenet-f', name: 'Google WaveNet English (US) Female', language: 'en-US', provider: 'google', gender: 'female' },
|
|
38
|
+
{ id: 'google-en-gb-wavenet-a', name: 'Google WaveNet English (UK)', language: 'en-GB', provider: 'google', gender: 'female' },
|
|
39
|
+
{ id: 'google-fr-wavenet-a', name: 'Google WaveNet French', language: 'fr', provider: 'google', gender: 'female' },
|
|
40
|
+
{ id: 'google-de-wavenet-a', name: 'Google WaveNet German', language: 'de', provider: 'google', gender: 'female' },
|
|
41
|
+
{ id: 'google-es-wavenet-a', name: 'Google WaveNet Spanish', language: 'es', provider: 'google', gender: 'female' },
|
|
42
|
+
{ id: 'google-ja-wavenet-a', name: 'Google WaveNet Japanese', language: 'ja', provider: 'google', gender: 'female' },
|
|
43
|
+
{ id: 'google-hi-wavenet-a', name: 'Google WaveNet Hindi', language: 'hi', provider: 'google', gender: 'female' },
|
|
44
|
+
{ id: 'google-ar-wavenet-a', name: 'Google WaveNet Arabic', language: 'ar', provider: 'google', gender: 'female' },
|
|
45
|
+
|
|
46
|
+
// ElevenLabs voices
|
|
47
|
+
{ id: 'elevenlabs-rachel', name: 'Rachel', language: 'en', provider: 'elevenlabs', gender: 'female' },
|
|
48
|
+
{ id: 'elevenlabs-adam', name: 'Adam', language: 'en', provider: 'elevenlabs', gender: 'male' },
|
|
49
|
+
{ id: 'elevenlabs-antoni', name: 'Antoni', language: 'en', provider: 'elevenlabs', gender: 'male' },
|
|
50
|
+
{ id: 'elevenlabs-bella', name: 'Bella', language: 'es', provider: 'elevenlabs', gender: 'female' },
|
|
51
|
+
{ id: 'elevenlabs-elli', name: 'Elli', language: 'de', provider: 'elevenlabs', gender: 'female' },
|
|
52
|
+
|
|
53
|
+
// OpenAI TTS voices
|
|
54
|
+
{ id: 'openai-alloy', name: 'Alloy', language: 'en', provider: 'openai' },
|
|
55
|
+
{ id: 'openai-echo', name: 'Echo', language: 'en', provider: 'openai', gender: 'male' },
|
|
56
|
+
{ id: 'openai-fable', name: 'Fable', language: 'en', provider: 'openai', gender: 'male' },
|
|
57
|
+
{ id: 'openai-onyx', name: 'Onyx', language: 'en', provider: 'openai', gender: 'male' },
|
|
58
|
+
{ id: 'openai-nova', name: 'Nova', language: 'en', provider: 'openai', gender: 'female' },
|
|
59
|
+
{ id: 'openai-shimmer', name: 'Shimmer', language: 'en', provider: 'openai', gender: 'female' },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all voices that match a given language code.
|
|
64
|
+
* Matches both exact codes and prefix codes (e.g., 'en' matches 'en', 'en-US', 'en-GB').
|
|
65
|
+
*/
|
|
66
|
+
export function getVoicesForLanguage(langCode: string): VoiceOption[] {
|
|
67
|
+
return VOICES.filter((v) => v.language === langCode || v.language.startsWith(langCode + '-') || langCode.startsWith(v.language + '-') || v.language.split('-')[0] === langCode)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the best voice for a language, optionally preferring a specific provider.
|
|
72
|
+
* Returns undefined if no voice matches.
|
|
73
|
+
*/
|
|
74
|
+
export function getBestVoice(langCode: string, preferredProvider?: VoiceOption['provider']): VoiceOption | undefined {
|
|
75
|
+
const voices = getVoicesForLanguage(langCode)
|
|
76
|
+
if (voices.length === 0) return undefined
|
|
77
|
+
|
|
78
|
+
if (preferredProvider) {
|
|
79
|
+
const preferred = voices.find((v) => v.provider === preferredProvider)
|
|
80
|
+
if (preferred) return preferred
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Return first available voice
|
|
84
|
+
return voices[0]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all voices from a specific provider.
|
|
89
|
+
*/
|
|
90
|
+
export function getVoicesByProvider(provider: VoiceOption['provider']): VoiceOption[] {
|
|
91
|
+
return VOICES.filter((v) => v.provider === provider)
|
|
92
|
+
}
|
package/dist/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ export * from './astro-link-card/index.ts';
|
|
|
64
64
|
export * from './astro-live-captions/index.ts';
|
|
65
65
|
export * from './astro-live-cursors/index.ts';
|
|
66
66
|
export * from './astro-live-transcript/index.ts';
|
|
67
|
+
export * from './astro-location-selector/index.ts';
|
|
67
68
|
export * from './astro-logger/index.ts';
|
|
68
69
|
export * from './astro-markdown-renderer/index.ts';
|
|
69
70
|
export * from './astro-marquee-strip/index.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './location-selector.ts'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { COUNTRIES, LANGUAGES, detectCountry, detectLanguage } from '../i18n/index.ts'
|
|
2
|
+
import { generateId } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export interface LocationSelectorState {
|
|
5
|
+
country: string
|
|
6
|
+
language: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LocationSelectorOptions {
|
|
10
|
+
defaultCountry?: string
|
|
11
|
+
defaultLanguage?: string
|
|
12
|
+
onCountryChange?: (country: string) => void
|
|
13
|
+
onLanguageChange?: (language: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createLocationSelector(options: LocationSelectorOptions = {}) {
|
|
17
|
+
const {
|
|
18
|
+
defaultCountry = 'US',
|
|
19
|
+
defaultLanguage = 'en',
|
|
20
|
+
onCountryChange,
|
|
21
|
+
onLanguageChange,
|
|
22
|
+
} = options
|
|
23
|
+
|
|
24
|
+
let state: LocationSelectorState = {
|
|
25
|
+
country: defaultCountry,
|
|
26
|
+
language: defaultLanguage,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const countryId = generateId('location-sel-country')
|
|
30
|
+
const languageId = generateId('location-sel-language')
|
|
31
|
+
|
|
32
|
+
function setCountry(country: string) {
|
|
33
|
+
state.country = country
|
|
34
|
+
onCountryChange?.(country)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setLanguage(language: string) {
|
|
38
|
+
state.language = language
|
|
39
|
+
onLanguageChange?.(language)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const countries = Array.isArray(COUNTRIES) ? COUNTRIES : Object.values(COUNTRIES)
|
|
43
|
+
const languages = Array.isArray(LANGUAGES) ? LANGUAGES : Object.values(LANGUAGES)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
state,
|
|
47
|
+
setCountry,
|
|
48
|
+
setLanguage,
|
|
49
|
+
countries,
|
|
50
|
+
languages,
|
|
51
|
+
countryProps: {
|
|
52
|
+
id: countryId,
|
|
53
|
+
name: 'country',
|
|
54
|
+
},
|
|
55
|
+
languageProps: {
|
|
56
|
+
id: languageId,
|
|
57
|
+
name: 'language',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,127 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Emoji system — shortcode to unicode mapping and search.
|
|
3
|
+
*
|
|
4
|
+
* The map is derived from the single shared emoji dataset in
|
|
5
|
+
* `@refraction-ui/emoji-picker` (`buildShortcodeMap`), so rich-editor, the
|
|
6
|
+
* emoji picker, and the composer's `:` trigger all resolve `:shortcode:` the
|
|
7
|
+
* same way — the ~120-entry hand-list that used to live here (and diverge from
|
|
8
|
+
* the picker) is gone. Popular aliases (`:smile:`, `:joy:`, `:100:`) live in
|
|
9
|
+
* the shared dataset's `SHORTCODE_ALIASES`.
|
|
3
10
|
*/
|
|
11
|
+
import { buildShortcodeMap } from '../emoji-picker/index.ts'
|
|
4
12
|
|
|
5
13
|
// ---------------------------------------------------------------------------
|
|
6
14
|
// Emoji map
|
|
7
15
|
// ---------------------------------------------------------------------------
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
':laugh:': '\u{1F602}',
|
|
12
|
-
':joy:': '\u{1F602}',
|
|
13
|
-
':grin:': '\u{1F601}',
|
|
14
|
-
':wink:': '\u{1F609}',
|
|
15
|
-
':blush:': '\u{1F60A}',
|
|
16
|
-
':heart_eyes:': '\u{1F60D}',
|
|
17
|
-
':kissing:': '\u{1F617}',
|
|
18
|
-
':thinking:': '\u{1F914}',
|
|
19
|
-
':neutral:': '\u{1F610}',
|
|
20
|
-
':expressionless:': '\u{1F611}',
|
|
21
|
-
':unamused:': '\u{1F612}',
|
|
22
|
-
':sweat:': '\u{1F613}',
|
|
23
|
-
':pensive:': '\u{1F614}',
|
|
24
|
-
':confused:': '\u{1F615}',
|
|
25
|
-
':disappointed:': '\u{1F61E}',
|
|
26
|
-
':worried:': '\u{1F61F}',
|
|
27
|
-
':angry:': '\u{1F620}',
|
|
28
|
-
':rage:': '\u{1F621}',
|
|
29
|
-
':cry:': '\u{1F622}',
|
|
30
|
-
':sob:': '\u{1F62D}',
|
|
31
|
-
':scream:': '\u{1F631}',
|
|
32
|
-
':fearful:': '\u{1F628}',
|
|
33
|
-
':cold_sweat:': '\u{1F630}',
|
|
34
|
-
':relieved:': '\u{1F60C}',
|
|
35
|
-
':sleepy:': '\u{1F62A}',
|
|
36
|
-
':sleeping:': '\u{1F634}',
|
|
37
|
-
':mask:': '\u{1F637}',
|
|
38
|
-
':sunglasses:': '\u{1F60E}',
|
|
39
|
-
':nerd:': '\u{1F913}',
|
|
40
|
-
':heart:': '\u{2764}\u{FE0F}',
|
|
41
|
-
':broken_heart:': '\u{1F494}',
|
|
42
|
-
':sparkling_heart:': '\u{1F496}',
|
|
43
|
-
':two_hearts:': '\u{1F495}',
|
|
44
|
-
':fire:': '\u{1F525}',
|
|
45
|
-
':100:': '\u{1F4AF}',
|
|
46
|
-
':star:': '\u{2B50}',
|
|
47
|
-
':star2:': '\u{1F31F}',
|
|
48
|
-
':sparkles:': '\u{2728}',
|
|
49
|
-
':thumbsup:': '\u{1F44D}',
|
|
50
|
-
':thumbsdown:': '\u{1F44E}',
|
|
51
|
-
':wave:': '\u{1F44B}',
|
|
52
|
-
':clap:': '\u{1F44F}',
|
|
53
|
-
':raised_hands:': '\u{1F64C}',
|
|
54
|
-
':pray:': '\u{1F64F}',
|
|
55
|
-
':muscle:': '\u{1F4AA}',
|
|
56
|
-
':point_up:': '\u{261D}\u{FE0F}',
|
|
57
|
-
':point_down:': '\u{1F447}',
|
|
58
|
-
':point_left:': '\u{1F448}',
|
|
59
|
-
':point_right:': '\u{1F449}',
|
|
60
|
-
':ok_hand:': '\u{1F44C}',
|
|
61
|
-
':v:': '\u{270C}\u{FE0F}',
|
|
62
|
-
':eyes:': '\u{1F440}',
|
|
63
|
-
':tongue:': '\u{1F445}',
|
|
64
|
-
':lips:': '\u{1F444}',
|
|
65
|
-
':rocket:': '\u{1F680}',
|
|
66
|
-
':tada:': '\u{1F389}',
|
|
67
|
-
':confetti:': '\u{1F38A}',
|
|
68
|
-
':balloon:': '\u{1F388}',
|
|
69
|
-
':gift:': '\u{1F381}',
|
|
70
|
-
':trophy:': '\u{1F3C6}',
|
|
71
|
-
':medal:': '\u{1F3C5}',
|
|
72
|
-
':crown:': '\u{1F451}',
|
|
73
|
-
':gem:': '\u{1F48E}',
|
|
74
|
-
':bell:': '\u{1F514}',
|
|
75
|
-
':check:': '\u{2705}',
|
|
76
|
-
':x:': '\u{274C}',
|
|
77
|
-
':warning:': '\u{26A0}\u{FE0F}',
|
|
78
|
-
':no_entry:': '\u{26D4}',
|
|
79
|
-
':question:': '\u{2753}',
|
|
80
|
-
':exclamation:': '\u{2757}',
|
|
81
|
-
':bulb:': '\u{1F4A1}',
|
|
82
|
-
':book:': '\u{1F4D6}',
|
|
83
|
-
':memo:': '\u{1F4DD}',
|
|
84
|
-
':pencil:': '\u{270F}\u{FE0F}',
|
|
85
|
-
':calendar:': '\u{1F4C5}',
|
|
86
|
-
':clock:': '\u{1F550}',
|
|
87
|
-
':hourglass:': '\u{231B}',
|
|
88
|
-
':lock:': '\u{1F512}',
|
|
89
|
-
':key:': '\u{1F511}',
|
|
90
|
-
':hammer:': '\u{1F528}',
|
|
91
|
-
':wrench:': '\u{1F527}',
|
|
92
|
-
':link:': '\u{1F517}',
|
|
93
|
-
':paperclip:': '\u{1F4CE}',
|
|
94
|
-
':scissors:': '\u{2702}\u{FE0F}',
|
|
95
|
-
':package:': '\u{1F4E6}',
|
|
96
|
-
':email:': '\u{1F4E7}',
|
|
97
|
-
':phone:': '\u{1F4DE}',
|
|
98
|
-
':computer:': '\u{1F4BB}',
|
|
99
|
-
':house:': '\u{1F3E0}',
|
|
100
|
-
':tree:': '\u{1F333}',
|
|
101
|
-
':sun:': '\u{2600}\u{FE0F}',
|
|
102
|
-
':moon:': '\u{1F319}',
|
|
103
|
-
':cloud:': '\u{2601}\u{FE0F}',
|
|
104
|
-
':rain:': '\u{1F327}\u{FE0F}',
|
|
105
|
-
':snow:': '\u{2744}\u{FE0F}',
|
|
106
|
-
':zap:': '\u{26A1}',
|
|
107
|
-
':rainbow:': '\u{1F308}',
|
|
108
|
-
':pizza:': '\u{1F355}',
|
|
109
|
-
':hamburger:': '\u{1F354}',
|
|
110
|
-
':coffee:': '\u{2615}',
|
|
111
|
-
':beer:': '\u{1F37A}',
|
|
112
|
-
':wine:': '\u{1F377}',
|
|
113
|
-
':cake:': '\u{1F370}',
|
|
114
|
-
':dog:': '\u{1F436}',
|
|
115
|
-
':cat:': '\u{1F431}',
|
|
116
|
-
':poop:': '\u{1F4A9}',
|
|
117
|
-
':ghost:': '\u{1F47B}',
|
|
118
|
-
':skull:': '\u{1F480}',
|
|
119
|
-
':alien:': '\u{1F47D}',
|
|
120
|
-
':robot:': '\u{1F916}',
|
|
121
|
-
':handshake:': '\u{1F91D}',
|
|
122
|
-
':pray2:': '\u{1F64F}',
|
|
123
|
-
':salute:': '\u{1FAE1}',
|
|
124
|
-
}
|
|
17
|
+
/** `:shortcode:` → glyph, built from the shared dataset (full set + aliases). */
|
|
18
|
+
export const EMOJI_MAP: Record<string, string> = buildShortcodeMap()
|
|
125
19
|
|
|
126
20
|
// ---------------------------------------------------------------------------
|
|
127
21
|
// Detection and search
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refraction-ui/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "All Refraction UI Astro components in one package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"@refraction-ui/astro-live-captions": "workspace:*",
|
|
85
85
|
"@refraction-ui/astro-live-cursors": "workspace:*",
|
|
86
86
|
"@refraction-ui/astro-live-transcript": "workspace:*",
|
|
87
|
+
"@refraction-ui/astro-location-selector": "workspace:*",
|
|
87
88
|
"@refraction-ui/astro-logger": "workspace:*",
|
|
88
89
|
"@refraction-ui/astro-markdown-renderer": "workspace:*",
|
|
89
90
|
"@refraction-ui/astro-marquee-strip": "workspace:*",
|