langie 1.9.25 → 1.9.26
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/components/InterfaceLanguageSelect.vue.d.ts +65 -0
- package/dist/components/LanguageSelect.vue.d.ts +59 -0
- package/dist/components/index.cjs +66 -39
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.css +81 -116
- package/dist/components/index.css.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.mjs +66 -39
- package/dist/components/index.mjs.map +1 -1
- package/dist/components/lt.vue.d.ts +37 -0
- package/dist/components/types.d.ts +20 -0
- package/dist/composables/types.d.ts +8 -0
- package/dist/composables/useLangie-batching.d.ts +48 -0
- package/dist/composables/useLangie-core.d.ts +21 -0
- package/dist/constants/colors.d.ts +82 -0
- package/dist/constants.d.ts +29 -0
- package/dist/core.cjs +17 -8
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.ts +10 -85
- package/dist/core.mjs +16 -8
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +52 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +81 -81
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +19 -163
- package/dist/index.mjs +52 -45
- package/dist/index.mjs.map +1 -1
- package/dist/language-aliases/african.d.ts +2 -0
- package/dist/language-aliases/asian.d.ts +2 -0
- package/dist/language-aliases/european.d.ts +2 -0
- package/dist/language-aliases/index.d.ts +3 -0
- package/dist/language-aliases/types.d.ts +5 -0
- package/dist/language-aliases.d.ts +1 -0
- package/dist/search-utils.d.ts +4 -0
- package/dist/{core.d.cts → types.d.ts} +5 -39
- package/dist/useLangie.d.ts +55 -0
- package/dist/utils/cache.d.ts +33 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/getCountryCode.d.ts +3 -0
- package/dist/utils/languageMapping.d.ts +13 -0
- package/dist/utils/regionMapping.d.ts +18 -0
- package/dist/utils/theme.d.ts +9 -0
- package/package.json +9 -5
- package/dist/index.d.cts +0 -165
- package/dist/teleport.d.cts +0 -2
- package/dist/teleport.d.ts +0 -2
|
@@ -1,54 +1,19 @@
|
|
|
1
|
-
/* src/
|
|
2
|
-
.
|
|
3
|
-
.multiselect-option {
|
|
4
|
-
display: flex;
|
|
5
|
-
align-items: center;
|
|
6
|
-
}
|
|
7
|
-
.lang-flag {
|
|
8
|
-
width: 20px;
|
|
9
|
-
height: 15px;
|
|
10
|
-
margin-right: 10px;
|
|
11
|
-
border: 1px solid var(--langie-flag-border, #eee);
|
|
12
|
-
border-radius: 2px;
|
|
13
|
-
object-fit: cover;
|
|
14
|
-
}
|
|
15
|
-
.is-dark .lang-flag {
|
|
16
|
-
border-color: var(--langie-flag-border-dark, #4b5563);
|
|
17
|
-
}
|
|
18
|
-
.native-name {
|
|
19
|
-
color: var(--langie-text-secondary, #6b7280);
|
|
20
|
-
margin-left: 0.5rem;
|
|
21
|
-
font-size: 0.9em;
|
|
22
|
-
}
|
|
23
|
-
.is-dark .native-name {
|
|
24
|
-
color: var(--langie-text-secondary-dark, #9ca3af);
|
|
25
|
-
}
|
|
26
|
-
.multiselect-no-results,
|
|
27
|
-
.multiselect-no-options {
|
|
28
|
-
padding: 0.5rem 0.875rem;
|
|
29
|
-
color: var(--langie-text-secondary, #6b7280);
|
|
30
|
-
}
|
|
31
|
-
.is-dark .multiselect-no-results,
|
|
32
|
-
.is-dark .multiselect-no-options {
|
|
33
|
-
color: var(--langie-text-secondary-dark, #9ca3af);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* vue-style:/Users/nlit/projects/langie-sdk/src/components/LanguageSelect.vue?type=style&index=0 */
|
|
37
|
-
.language-select[data-v-25bc3fde] {
|
|
1
|
+
/* vue-style:/builds/codelits/langie-sdk/src/components/LanguageSelect.vue?type=style&index=0 */
|
|
2
|
+
.language-select[data-v-5b09cc17] {
|
|
38
3
|
position: relative;
|
|
39
4
|
min-height: 40px;
|
|
40
5
|
}
|
|
41
|
-
.skeleton-loader[data-v-
|
|
6
|
+
.skeleton-loader[data-v-5b09cc17] {
|
|
42
7
|
width: 100%;
|
|
43
8
|
height: 40px;
|
|
44
9
|
background-color: #e2e8f0;
|
|
45
10
|
border-radius: 4px;
|
|
46
|
-
animation: pulse-
|
|
11
|
+
animation: pulse-5b09cc17 1.5s infinite ease-in-out;
|
|
47
12
|
}
|
|
48
|
-
.is-dark .skeleton-loader[data-v-
|
|
13
|
+
.is-dark .skeleton-loader[data-v-5b09cc17] {
|
|
49
14
|
background-color: #374151;
|
|
50
15
|
}
|
|
51
|
-
@keyframes pulse-
|
|
16
|
+
@keyframes pulse-5b09cc17 {
|
|
52
17
|
0%, 100% {
|
|
53
18
|
opacity: 1;
|
|
54
19
|
}
|
|
@@ -56,64 +21,64 @@
|
|
|
56
21
|
opacity: 0.5;
|
|
57
22
|
}
|
|
58
23
|
}
|
|
59
|
-
[data-v-
|
|
24
|
+
[data-v-5b09cc17] .multiselect {
|
|
60
25
|
--ms-font-size: 1rem;
|
|
61
26
|
--ms-line-height: 1.5;
|
|
62
|
-
--ms-bg: var(--
|
|
63
|
-
--ms-bg-disabled: var(--
|
|
64
|
-
--ms-border-color: var(--
|
|
27
|
+
--ms-bg: var(--5b09cc17-THEME_COLORS\.light\.background);
|
|
28
|
+
--ms-bg-disabled: var(--5b09cc17-THEME_COLORS\.light\.backgroundDisabled);
|
|
29
|
+
--ms-border-color: var(--5b09cc17-THEME_COLORS\.light\.border);
|
|
65
30
|
--ms-border-width: 1px;
|
|
66
31
|
--ms-radius: 4px;
|
|
67
32
|
--ms-py: 0.3rem;
|
|
68
33
|
--ms-px: 0.875rem;
|
|
69
34
|
--ms-ring-width: 3px;
|
|
70
|
-
--ms-ring-color: var(--
|
|
71
|
-
--ms-placeholder-color: var(--
|
|
35
|
+
--ms-ring-color: var(--5b09cc17-THEME_COLORS\.light\.ring);
|
|
36
|
+
--ms-placeholder-color: var(--5b09cc17-THEME_COLORS\.light\.placeholder);
|
|
72
37
|
--ms-color: #1f2937;
|
|
73
38
|
--ms-option-font-size: 1rem;
|
|
74
39
|
--ms-option-line-height: 1;
|
|
75
40
|
--ms-option-py: 0.2rem;
|
|
76
41
|
--ms-option-px: 0.5rem;
|
|
77
|
-
--ms-option-bg-pointed: var(--
|
|
78
|
-
--ms-option-color-pointed: var(--
|
|
79
|
-
--ms-option-bg-selected: var(--
|
|
80
|
-
--ms-option-color-selected: var(--
|
|
42
|
+
--ms-option-bg-pointed: var(--5b09cc17-THEME_COLORS\.light\.optionPointed);
|
|
43
|
+
--ms-option-color-pointed: var(--5b09cc17-THEME_COLORS\.light\.optionPointedText);
|
|
44
|
+
--ms-option-bg-selected: var(--5b09cc17-THEME_COLORS\.light\.optionSelected);
|
|
45
|
+
--ms-option-color-selected: var(--5b09cc17-THEME_COLORS\.light\.optionSelectedText);
|
|
81
46
|
--ms-option-bg-disabled: #fff;
|
|
82
47
|
--ms-option-color-disabled: #d1d5db;
|
|
83
|
-
--ms-dropdown-bg: var(--
|
|
84
|
-
--ms-dropdown-border-color: var(--
|
|
85
|
-
--ms-dropdown-color: var(--
|
|
86
|
-
--ms-tag-bg: var(--
|
|
87
|
-
}
|
|
88
|
-
.is-dark[data-v-
|
|
89
|
-
--ms-bg: var(--
|
|
90
|
-
--ms-bg-disabled: var(--
|
|
91
|
-
--ms-border-color: var(--
|
|
92
|
-
--ms-placeholder-color: var(--
|
|
93
|
-
--ms-ring-color: var(--
|
|
94
|
-
--ms-option-bg-pointed: var(--
|
|
95
|
-
--ms-option-color-pointed: var(--
|
|
96
|
-
--ms-option-bg-selected: var(--
|
|
97
|
-
--ms-option-color-selected: var(--
|
|
98
|
-
--ms-dropdown-bg: var(--
|
|
99
|
-
--ms-dropdown-border-color: var(--
|
|
100
|
-
--ms-dropdown-color: var(--
|
|
101
|
-
}
|
|
102
|
-
[data-v-
|
|
48
|
+
--ms-dropdown-bg: var(--5b09cc17-THEME_COLORS\.light\.dropdownBackground);
|
|
49
|
+
--ms-dropdown-border-color: var(--5b09cc17-THEME_COLORS\.light\.dropdownBorder);
|
|
50
|
+
--ms-dropdown-color: var(--5b09cc17-THEME_COLORS\.light\.dropdownText);
|
|
51
|
+
--ms-tag-bg: var(--5b09cc17-THEME_COLORS\.light\.tagBackground);
|
|
52
|
+
}
|
|
53
|
+
.is-dark[data-v-5b09cc17] .multiselect {
|
|
54
|
+
--ms-bg: var(--5b09cc17-THEME_COLORS\.dark\.background);
|
|
55
|
+
--ms-bg-disabled: var(--5b09cc17-THEME_COLORS\.dark\.backgroundDisabled);
|
|
56
|
+
--ms-border-color: var(--5b09cc17-THEME_COLORS\.dark\.border);
|
|
57
|
+
--ms-placeholder-color: var(--5b09cc17-THEME_COLORS\.dark\.placeholder);
|
|
58
|
+
--ms-ring-color: var(--5b09cc17-THEME_COLORS\.dark\.ring);
|
|
59
|
+
--ms-option-bg-pointed: var(--5b09cc17-THEME_COLORS\.dark\.optionPointed);
|
|
60
|
+
--ms-option-color-pointed: var(--5b09cc17-THEME_COLORS\.dark\.optionPointedText);
|
|
61
|
+
--ms-option-bg-selected: var(--5b09cc17-THEME_COLORS\.dark\.optionSelected);
|
|
62
|
+
--ms-option-color-selected: var(--5b09cc17-THEME_COLORS\.dark\.optionSelectedText);
|
|
63
|
+
--ms-dropdown-bg: var(--5b09cc17-THEME_COLORS\.dark\.dropdownBackground);
|
|
64
|
+
--ms-dropdown-border-color: var(--5b09cc17-THEME_COLORS\.dark\.dropdownBorder);
|
|
65
|
+
--ms-dropdown-color: var(--5b09cc17-THEME_COLORS\.dark\.dropdownText);
|
|
66
|
+
}
|
|
67
|
+
[data-v-5b09cc17] .lang-flag {
|
|
103
68
|
width: 20px;
|
|
104
69
|
height: 15px;
|
|
105
|
-
border: 1px solid var(--
|
|
70
|
+
border: 1px solid var(--5b09cc17-COLORS\.border\.flag);
|
|
106
71
|
border-radius: 2px;
|
|
107
72
|
object-fit: cover;
|
|
108
73
|
flex-shrink: 0;
|
|
109
74
|
display: block;
|
|
110
75
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
111
76
|
}
|
|
112
|
-
.is-dark[data-v-
|
|
113
|
-
border-color: var(--
|
|
77
|
+
.is-dark[data-v-5b09cc17] .lang-flag {
|
|
78
|
+
border-color: var(--5b09cc17-COLORS\.border\.dark);
|
|
114
79
|
}
|
|
115
|
-
[data-v-
|
|
116
|
-
[data-v-
|
|
80
|
+
[data-v-5b09cc17] .multiselect-single-label,
|
|
81
|
+
[data-v-5b09cc17] .multiselect-option {
|
|
117
82
|
display: flex !important;
|
|
118
83
|
align-items: center !important;
|
|
119
84
|
min-width: 0 !important;
|
|
@@ -124,14 +89,14 @@
|
|
|
124
89
|
overflow: hidden !important;
|
|
125
90
|
flex-wrap: nowrap !important;
|
|
126
91
|
}
|
|
127
|
-
[data-v-
|
|
92
|
+
[data-v-5b09cc17] .multiselect-option {
|
|
128
93
|
text-align: left !important;
|
|
129
94
|
transition: background-color 0.15s ease !important;
|
|
130
95
|
white-space: nowrap !important;
|
|
131
96
|
overflow: hidden !important;
|
|
132
97
|
flex-wrap: nowrap !important;
|
|
133
98
|
}
|
|
134
|
-
[data-v-
|
|
99
|
+
[data-v-5b09cc17] .language-text {
|
|
135
100
|
display: inline-block !important;
|
|
136
101
|
min-width: 0 !important;
|
|
137
102
|
white-space: nowrap !important;
|
|
@@ -139,47 +104,47 @@
|
|
|
139
104
|
text-overflow: ellipsis !important;
|
|
140
105
|
flex: 1 !important;
|
|
141
106
|
line-height: 1.4 !important;
|
|
142
|
-
color: var(--
|
|
107
|
+
color: var(--5b09cc17-THEME_COLORS\.light\.dropdownText) !important;
|
|
143
108
|
word-break: keep-all !important;
|
|
144
109
|
word-wrap: normal !important;
|
|
145
110
|
word-spacing: normal !important;
|
|
146
111
|
max-width: 100% !important;
|
|
147
112
|
}
|
|
148
|
-
.is-dark[data-v-
|
|
149
|
-
color: var(--
|
|
113
|
+
.is-dark[data-v-5b09cc17] .language-text {
|
|
114
|
+
color: var(--5b09cc17-THEME_COLORS\.dark\.dropdownText) !important;
|
|
150
115
|
}
|
|
151
|
-
[data-v-
|
|
152
|
-
color: var(--
|
|
116
|
+
[data-v-5b09cc17] .native-name {
|
|
117
|
+
color: var(--5b09cc17-COLORS\.text\.secondary);
|
|
153
118
|
margin-left: 0.5rem;
|
|
154
119
|
font-size: 0.9em;
|
|
155
120
|
white-space: nowrap !important;
|
|
156
121
|
display: inline !important;
|
|
157
122
|
}
|
|
158
|
-
.is-dark[data-v-
|
|
159
|
-
color: var(--
|
|
123
|
+
.is-dark[data-v-5b09cc17] .native-name {
|
|
124
|
+
color: var(--5b09cc17-COLORS\.neutral\.gray400);
|
|
160
125
|
}
|
|
161
|
-
[data-v-
|
|
126
|
+
[data-v-5b09cc17] .multiselect {
|
|
162
127
|
width: 100%;
|
|
163
128
|
min-width: 200px;
|
|
164
|
-
color: var(--
|
|
129
|
+
color: var(--5b09cc17-THEME_COLORS\.light\.dropdownText);
|
|
165
130
|
position: relative;
|
|
166
131
|
z-index: 1;
|
|
167
132
|
}
|
|
168
|
-
.is-dark[data-v-
|
|
169
|
-
color: var(--
|
|
133
|
+
.is-dark[data-v-5b09cc17] .multiselect {
|
|
134
|
+
color: var(--5b09cc17-THEME_COLORS\.dark\.dropdownText);
|
|
170
135
|
}
|
|
171
|
-
[data-v-
|
|
136
|
+
[data-v-5b09cc17] .multiselect-dropdown {
|
|
172
137
|
padding: 2px 0 !important;
|
|
173
138
|
z-index: 1000 !important;
|
|
174
139
|
}
|
|
175
|
-
[data-v-
|
|
140
|
+
[data-v-5b09cc17] .multiselect-dropdown .multiselect-option {
|
|
176
141
|
padding: 4px 8px !important;
|
|
177
142
|
margin: 0 !important;
|
|
178
143
|
white-space: nowrap !important;
|
|
179
144
|
overflow: hidden !important;
|
|
180
145
|
flex-wrap: nowrap !important;
|
|
181
146
|
}
|
|
182
|
-
[data-v-
|
|
147
|
+
[data-v-5b09cc17] .multiselect-dropdown .language-text {
|
|
183
148
|
white-space: nowrap !important;
|
|
184
149
|
overflow: hidden !important;
|
|
185
150
|
text-overflow: ellipsis !important;
|
|
@@ -188,36 +153,36 @@
|
|
|
188
153
|
word-spacing: normal !important;
|
|
189
154
|
max-width: 100% !important;
|
|
190
155
|
}
|
|
191
|
-
[data-v-
|
|
192
|
-
[data-v-
|
|
156
|
+
[data-v-5b09cc17] .multiselect-no-results,
|
|
157
|
+
[data-v-5b09cc17] .multiselect-no-options {
|
|
193
158
|
padding: 0.5rem 0.875rem;
|
|
194
|
-
color: var(--
|
|
159
|
+
color: var(--5b09cc17-COLORS\.text\.secondary);
|
|
195
160
|
}
|
|
196
|
-
.is-dark[data-v-
|
|
197
|
-
.is-dark[data-v-
|
|
198
|
-
color: var(--
|
|
161
|
+
.is-dark[data-v-5b09cc17] .multiselect-no-results,
|
|
162
|
+
.is-dark[data-v-5b09cc17] .multiselect-no-options {
|
|
163
|
+
color: var(--5b09cc17-COLORS\.neutral\.gray400);
|
|
199
164
|
}
|
|
200
165
|
|
|
201
|
-
/* vue-style:/
|
|
202
|
-
.interface-language-select-wrapper[data-v-
|
|
166
|
+
/* vue-style:/builds/codelits/langie-sdk/src/components/InterfaceLanguageSelect.vue?type=style&index=0 */
|
|
167
|
+
.interface-language-select-wrapper[data-v-258d337a] {
|
|
203
168
|
position: relative;
|
|
204
169
|
display: inline-block;
|
|
205
170
|
width: 100%;
|
|
206
171
|
min-width: 200px;
|
|
207
172
|
}
|
|
208
|
-
.interface-language-select-wrapper[data-v-
|
|
173
|
+
.interface-language-select-wrapper[data-v-258d337a] .multiselect-option {
|
|
209
174
|
padding: 5px 10px !important;
|
|
210
175
|
margin: 0;
|
|
211
176
|
border-radius: 0;
|
|
212
177
|
}
|
|
213
|
-
.interface-language-select-wrapper[data-v-
|
|
178
|
+
.interface-language-select-wrapper[data-v-258d337a] .multiselect-option > div {
|
|
214
179
|
padding: 0 !important;
|
|
215
180
|
margin: 0;
|
|
216
181
|
}
|
|
217
|
-
.interface-language-select-wrapper[data-v-
|
|
182
|
+
.interface-language-select-wrapper[data-v-258d337a] .multiselect-dropdown {
|
|
218
183
|
padding: 2px 0 !important;
|
|
219
184
|
}
|
|
220
|
-
.language-change-loader[data-v-
|
|
185
|
+
.language-change-loader[data-v-258d337a] {
|
|
221
186
|
position: absolute;
|
|
222
187
|
top: 0;
|
|
223
188
|
left: 0;
|
|
@@ -233,33 +198,33 @@
|
|
|
233
198
|
backdrop-filter: blur(2px);
|
|
234
199
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
235
200
|
}
|
|
236
|
-
.language-change-loader.is-dark[data-v-
|
|
201
|
+
.language-change-loader.is-dark[data-v-258d337a] {
|
|
237
202
|
background: rgba(30, 30, 30, 0.95);
|
|
238
203
|
border-color: rgba(255, 255, 255, 0.1);
|
|
239
204
|
}
|
|
240
|
-
.loader-spinner[data-v-
|
|
205
|
+
.loader-spinner[data-v-258d337a] {
|
|
241
206
|
width: 24px;
|
|
242
207
|
height: 24px;
|
|
243
208
|
border: 3px solid #e5e7eb;
|
|
244
209
|
border-top: 3px solid #3b82f6;
|
|
245
210
|
border-radius: 50%;
|
|
246
|
-
animation: spin-
|
|
211
|
+
animation: spin-258d337a 1s linear infinite;
|
|
247
212
|
margin-bottom: 8px;
|
|
248
213
|
}
|
|
249
|
-
.language-change-loader.is-dark .loader-spinner[data-v-
|
|
214
|
+
.language-change-loader.is-dark .loader-spinner[data-v-258d337a] {
|
|
250
215
|
border-color: #374151;
|
|
251
216
|
border-top-color: #3b82f6;
|
|
252
217
|
}
|
|
253
|
-
.loader-text[data-v-
|
|
218
|
+
.loader-text[data-v-258d337a] {
|
|
254
219
|
font-size: 12px;
|
|
255
220
|
color: #6b7280;
|
|
256
221
|
font-weight: 500;
|
|
257
222
|
text-align: center;
|
|
258
223
|
}
|
|
259
|
-
.language-change-loader.is-dark .loader-text[data-v-
|
|
224
|
+
.language-change-loader.is-dark .loader-text[data-v-258d337a] {
|
|
260
225
|
color: #9ca3af;
|
|
261
226
|
}
|
|
262
|
-
@keyframes spin-
|
|
227
|
+
@keyframes spin-258d337a {
|
|
263
228
|
0% {
|
|
264
229
|
transform: rotate(0deg);
|
|
265
230
|
}
|
|
@@ -268,13 +233,13 @@
|
|
|
268
233
|
}
|
|
269
234
|
}
|
|
270
235
|
|
|
271
|
-
/* vue-style:/
|
|
272
|
-
.fade-enter-active[data-v-
|
|
273
|
-
.fade-leave-active[data-v-
|
|
236
|
+
/* vue-style:/builds/codelits/langie-sdk/src/components/lt.vue?type=style&index=0 */
|
|
237
|
+
.fade-enter-active[data-v-3341698b],
|
|
238
|
+
.fade-leave-active[data-v-3341698b] {
|
|
274
239
|
transition: opacity 0.25s ease;
|
|
275
240
|
}
|
|
276
|
-
.fade-enter-from[data-v-
|
|
277
|
-
.fade-leave-to[data-v-
|
|
241
|
+
.fade-enter-from[data-v-3341698b],
|
|
242
|
+
.fade-leave-to[data-v-3341698b] {
|
|
278
243
|
opacity: 0;
|
|
279
244
|
}
|
|
280
245
|
/*# sourceMappingURL=index.css.map */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/styles/teleport.css","/Users/nlit/projects/langie-sdk/src/components/LanguageSelect.vue","/Users/nlit/projects/langie-sdk/src/components/InterfaceLanguageSelect.vue","/Users/nlit/projects/langie-sdk/src/components/lt.vue"],"sourcesContent":["/* Teleport styles for multiselect components */\n/* These styles are used by components that are teleported outside the normal DOM flow */\n\n.multiselect-single-label,\n.multiselect-option {\n display: flex;\n align-items: center;\n}\n\n.lang-flag {\n width: 20px;\n height: 15px;\n margin-right: 10px;\n border: 1px solid var(--langie-flag-border, #eee);\n border-radius: 2px;\n object-fit: cover;\n}\n\n.is-dark .lang-flag {\n border-color: var(--langie-flag-border-dark, #4b5563);\n}\n\n.native-name {\n color: var(--langie-text-secondary, #6b7280);\n margin-left: 0.5rem;\n font-size: 0.9em;\n}\n\n.is-dark .native-name {\n color: var(--langie-text-secondary-dark, #9ca3af);\n}\n\n.multiselect-no-results,\n.multiselect-no-options {\n padding: 0.5rem 0.875rem;\n color: var(--langie-text-secondary, #6b7280);\n}\n\n.is-dark .multiselect-no-results,\n.is-dark .multiselect-no-options {\n color: var(--langie-text-secondary-dark, #9ca3af);\n}\n","<template>\n <div class=\"language-select\" :class=\"{ 'is-dark': isDark }\">\n <!-- Show multiselect when we have languages OR when not loading -->\n <Multiselect\n v-if=\"validLanguages.length > 0 || !isLoading\"\n :key=\"multiselectKey\"\n v-model=\"selectedLanguage\"\n :options=\"filteredLanguages\"\n :searchable=\"true\"\n @search-change=\"handleSearch\"\n :can-clear=\"false\"\n :allow-empty=\"false\"\n :object=\"true\"\n :placeholder=\"validLanguages.length === 0 ? 'No languages available' : placeholder\"\n :disabled=\"props.disabled || validLanguages.length === 0\"\n :loading=\"isLoading\"\n track-by=\"name\"\n label=\"name\"\n value-prop=\"code\"\n :filter-results=\"false\"\n @keydown=\"handleKeydown\"\n >\n <template #singlelabel=\"{ value }\">\n <div v-if=\"value\" class=\"multiselect-single-label\">\n <img\n v-if=\"value.code\"\n :key=\"selectedLanguageKey\"\n :src=\"getFlagImageUrl(value)\"\n class=\"lang-flag\"\n :alt=\"`${value.native_name || value.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ value.name }}\n <span class=\"native-name\">({{ value.native_name || value.name }})</span></span\n >\n </div>\n </template>\n <template #option=\"{ option }\">\n <div class=\"multiselect-option\" :data-lang-code=\"option.code\">\n <img\n :key=\"`${option.code}-${option.flag_country || 'fallback'}`\"\n :src=\"getFlagImageUrl(option)\"\n class=\"lang-flag\"\n :alt=\"`${option.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ option.name }}\n <span class=\"native-name\">({{ option.native_name || option.name }})</span></span\n >\n </div>\n </template>\n <template #noresults>\n <div class=\"multiselect-no-results\">No languages found.</div>\n </template>\n <template #nooptions>\n <div class=\"multiselect-no-options\">\n {{\n validLanguages.length === 0\n ? 'Please provide languages via the :languages prop'\n : 'No languages available.'\n }}\n </div>\n </template>\n </Multiselect>\n <!-- Only show skeleton loader when actually loading AND we have no languages yet -->\n <div v-else-if=\"isLoading && validLanguages.length === 0\" class=\"skeleton-loader\"></div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport '@vueform/multiselect/themes/default.css'\nimport '../styles/teleport.css'\nimport { ref, computed, nextTick } from 'vue'\nimport type { PropType } from 'vue'\nimport Multiselect from '@vueform/multiselect'\nimport Fuse from 'fuse.js'\nimport { applyLanguageAlias } from '../search-utils'\nimport { THEME_COLORS, COLORS } from '../constants/colors'\nimport type { TranslatorLanguage } from '../types'\n\nconst getFlagCode = (lang: TranslatorLanguage): string => {\n const flagCode = lang.flag_country || lang.code\n return flagCode\n}\n\nconst getFlagImageUrl = (lang: TranslatorLanguage): string => {\n const flagCode = getFlagCode(lang)\n // Use local flags (downloaded during build for offline support and faster loading)\n // If not found, the onerror handler will use CDN fallback\n return `/flags/${flagCode}.svg`\n}\n\nconst props = defineProps({\n modelValue: {\n type: Object as PropType<TranslatorLanguage | null | undefined>,\n default: undefined\n },\n languages: {\n type: Array as PropType<TranslatorLanguage[]>,\n default: () => []\n },\n placeholder: {\n type: String,\n default: 'Select language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst isLoading = computed(() => {\n return props.languages.length <= 0\n})\nconst searchQuery = ref('')\n\nconst validLanguages = computed(() => {\n return props.languages.filter((lang) => lang && lang.code && lang.name && lang.native_name)\n})\n\nconst fuse = computed(() => {\n if (!validLanguages.value.length) return null\n\n return new Fuse(validLanguages.value, {\n keys: ['name', 'native_name', 'code'],\n includeScore: true,\n threshold: 0.6, // Use highest threshold to catch all cases\n isCaseSensitive: false,\n minMatchCharLength: 1 // Allow single character matches\n })\n})\n\nconst multiselectKey = computed(() => {\n // Create a key that changes when the language data structure changes\n const hasApiData = props.languages.length > 0 && props.languages[0].flag_country !== null\n return hasApiData ? 'api-data' : 'fallback-data'\n})\n\nconst selectedLanguageKey = computed(() => {\n if (!props.modelValue || !props.modelValue.code) return 'no-selection'\n const key = `${props.modelValue.code}-${props.modelValue.flag_country || 'fallback'}`\n return key\n})\n\nconst selectedLanguage = computed({\n get: () => props.modelValue || null,\n set: (value) => {\n if (value && value.code) {\n emit('update:modelValue', value)\n }\n }\n})\n\nconst filteredLanguages = computed(() => {\n const query = searchQuery.value.trim()\n\n let results: TranslatorLanguage[]\n if (!query) {\n // When no search query, return all valid languages (bypass Fuse.js)\n results = validLanguages.value\n } else {\n // Only use Fuse.js when there's an actual search query\n if (!fuse.value) return []\n\n // Get the most likely alias or the original term\n const aliasResult = applyLanguageAlias(query)\n const searchTerm = Array.isArray(aliasResult.primary)\n ? aliasResult.primary[0]\n : aliasResult.primary\n\n // Always use Fuse.js for the main search (both original query and alias result)\n const fuseResults = fuse.value.search(searchTerm)\n results = fuseResults.map((result) => result.item)\n\n // If the search term is different from query (alias was applied), also search the original query\n if (searchTerm !== query) {\n const originalResults = fuse.value.search(query)\n originalResults.forEach((result) => {\n if (!results.some((r) => r.code === result.item.code)) {\n results.push(result.item)\n }\n })\n }\n\n // Add suggested languages to expand the results\n if (aliasResult.suggestions.length > 0) {\n aliasResult.suggestions.forEach((suggestion) => {\n const suggestedLang = validLanguages.value.find((lang) => {\n const langName = lang.name.toLowerCase()\n const suggestion_lower = suggestion.toLowerCase()\n\n // Try multiple matching strategies\n return (\n langName === suggestion_lower || // Exact match\n langName.includes(suggestion_lower) || // Contains match\n lang.code.toLowerCase() === suggestion_lower || // Code match\n langName.startsWith(suggestion_lower) || // Starts with match\n // Handle common variations\n (suggestion_lower === 'tatar' && langName.includes('tatar')) ||\n (suggestion_lower === 'belarusian' &&\n (langName.includes('belarus') || langName.includes('belarusian'))) ||\n (suggestion_lower === 'moldovan' &&\n (langName.includes('moldov') || langName.includes('moldova')))\n )\n })\n if (suggestedLang && !results.some((r) => r.code === suggestedLang.code)) {\n results.push(suggestedLang)\n }\n })\n }\n\n // Special handling for very short queries (e.g. \"се\")\n if (query.length === 2) {\n const lowerQuery = query.toLowerCase()\n const manualResults = validLanguages.value.filter(\n (lang) =>\n lang.name.toLowerCase().startsWith(lowerQuery) ||\n lang.native_name.toLowerCase().startsWith(lowerQuery)\n )\n manualResults.forEach((lang) => {\n if (!results.some((r) => r.code === lang.code)) {\n results.push(lang)\n }\n })\n }\n }\n\n // Remove the currently selected language from the options\n const filtered = results.filter(\n (lang) => !props.modelValue || !props.modelValue.code || lang.code !== props.modelValue.code\n )\n\n return filtered\n})\n\nfunction handleSearch(query: string) {\n searchQuery.value = query\n\n // Auto-scroll to first result when typing\n if (query.trim()) {\n nextTick(() => {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption) {\n firstOption.scrollIntoView({ behavior: 'smooth', block: 'nearest' })\n // Highlight the first option\n firstOption.classList.add('multiselect-option-is-pointed')\n }\n })\n }\n}\n\n// Handle keyboard navigation\nfunction handleKeydown(event: Event) {\n const keyboardEvent = event as KeyboardEvent\n if (keyboardEvent.key === 'Tab' || keyboardEvent.key === 'Enter') {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption && searchQuery.value.trim()) {\n // Get the language data from the first option\n const langCode = firstOption.getAttribute('data-lang-code')\n if (langCode) {\n const language = validLanguages.value.find((lang) => lang.code === langCode)\n if (language) {\n selectedLanguage.value = language\n searchQuery.value = ''\n keyboardEvent.preventDefault()\n }\n }\n }\n }\n}\n\nconst onFlagError = (event: Event) => {\n const target = event.target as HTMLImageElement\n const currentSrc = target.src\n\n // If loading from local cache failed, try CDN\n if (currentSrc && currentSrc.includes('/flags/')) {\n const match = currentSrc.match(/\\/flags\\/([a-z]{2})\\.svg/)\n if (match) {\n const countryCode = match[1]\n // Fallback to CDN\n target.src = `https://flagcdn.com/${countryCode}.svg`\n // If CDN also fails, hide the image\n target.onerror = () => {\n target.style.display = 'none'\n }\n return\n }\n }\n\n // If CDN or any other source fails, hide the image\n target.style.display = 'none'\n}\n</script>\n\n<style scoped>\n.language-select {\n position: relative;\n min-height: 40px; /* To prevent layout shift while loading */\n}\n\n.skeleton-loader {\n width: 100%;\n height: 40px;\n background-color: #e2e8f0;\n border-radius: 4px;\n animation: pulse 1.5s infinite ease-in-out;\n}\n\n.is-dark .skeleton-loader {\n background-color: #374151;\n}\n\n@keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n:deep(.multiselect) {\n --ms-font-size: 1rem;\n --ms-line-height: 1.5;\n --ms-bg: v-bind('THEME_COLORS.light.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.light.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.light.border');\n --ms-border-width: 1px;\n --ms-radius: 4px;\n --ms-py: 0.3rem;\n --ms-px: 0.875rem;\n --ms-ring-width: 3px;\n --ms-ring-color: v-bind('THEME_COLORS.light.ring');\n --ms-placeholder-color: v-bind('THEME_COLORS.light.placeholder');\n --ms-color: #1f2937; /* Explicit text color for light theme */\n\n /* Options */\n --ms-option-font-size: 1rem;\n --ms-option-line-height: 1;\n --ms-option-py: 0.2rem;\n --ms-option-px: 0.5rem;\n --ms-option-bg-pointed: v-bind('THEME_COLORS.light.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.light.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.light.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.light.optionSelectedText');\n --ms-option-bg-disabled: #fff;\n --ms-option-color-disabled: #d1d5db;\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.light.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.light.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.light.dropdownText');\n\n /* Others */\n --ms-tag-bg: v-bind('THEME_COLORS.light.tagBackground');\n}\n\n.is-dark :deep(.multiselect) {\n --ms-bg: v-bind('THEME_COLORS.dark.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.dark.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.dark.border');\n --ms-placeholder-color: v-bind('THEME_COLORS.dark.placeholder');\n --ms-ring-color: v-bind('THEME_COLORS.dark.ring');\n\n /* Options */\n --ms-option-bg-pointed: v-bind('THEME_COLORS.dark.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.dark.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.dark.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.dark.optionSelectedText');\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.dark.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.dark.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.lang-flag) {\n width: 20px;\n height: 15px;\n border: 1px solid v-bind('COLORS.border.flag');\n border-radius: 2px;\n object-fit: cover;\n flex-shrink: 0;\n display: block;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n\n.is-dark :deep(.lang-flag) {\n border-color: v-bind('COLORS.border.dark');\n}\n\n:deep(.multiselect-single-label),\n:deep(.multiselect-option) {\n display: flex !important;\n align-items: center !important;\n min-width: 0 !important;\n width: 100% !important;\n padding: 4px 8px !important;\n gap: 8px !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-option) {\n text-align: left !important;\n transition: background-color 0.15s ease !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.language-text) {\n display: inline-block !important;\n min-width: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n flex: 1 !important;\n line-height: 1.4 !important;\n color: v-bind('THEME_COLORS.light.dropdownText') !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n.is-dark :deep(.language-text) {\n color: v-bind('THEME_COLORS.dark.dropdownText') !important;\n}\n\n:deep(.native-name) {\n color: v-bind('COLORS.text.secondary');\n margin-left: 0.5rem;\n font-size: 0.9em;\n white-space: nowrap !important;\n display: inline !important;\n}\n\n.is-dark :deep(.native-name) {\n color: v-bind('COLORS.neutral.gray400');\n}\n\n:deep(.multiselect) {\n width: 100%;\n min-width: 200px;\n color: v-bind('THEME_COLORS.light.dropdownText');\n position: relative;\n z-index: 1;\n}\n\n.is-dark :deep(.multiselect) {\n color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n z-index: 1000 !important;\n}\n\n:deep(.multiselect-dropdown .multiselect-option) {\n padding: 4px 8px !important;\n margin: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-dropdown .language-text) {\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n:deep(.multiselect-no-results),\n:deep(.multiselect-no-options) {\n padding: 0.5rem 0.875rem;\n color: v-bind('COLORS.text.secondary');\n}\n\n.is-dark :deep(.multiselect-no-results),\n.is-dark :deep(.multiselect-no-options) {\n color: v-bind('COLORS.neutral.gray400');\n}\n</style>\n","<template>\n <div class=\"interface-language-select-wrapper\">\n <LanguageSelect\n :key=\"`interface-lang-${effectiveLanguages.length}-${effectiveLanguages[0]?.code || 'empty'}`\"\n :model-value=\"currentLanguageObject\"\n :languages=\"effectiveLanguages\"\n :placeholder=\"props.placeholder\"\n :disabled=\"props.disabled || isChangingLanguage\"\n :is-dark=\"props.isDark\"\n @update:model-value=\"handleLanguageChange\"\n />\n\n <!-- Loading overlay -->\n <div\n v-if=\"isChangingLanguage\"\n class=\"language-change-loader\"\n :class=\"{ 'is-dark': props.isDark }\"\n >\n <div class=\"loader-spinner\"></div>\n <span class=\"loader-text\">{{ lr('Changing language...', 'ui') }}</span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, watch, onMounted, ref } from 'vue'\nimport LanguageSelect from './LanguageSelect.vue'\nimport { useLangie } from '../useLangie'\nimport type { TranslatorLanguage } from '../types'\nimport { getCountryCode } from '../utils/getCountryCode'\n\n// Loading state for language change\nconst isChangingLanguage = ref(false)\n\nconst props = defineProps({\n placeholder: {\n type: String,\n default: 'Select interface language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n },\n translatorHost: {\n type: String,\n default: ''\n },\n apiKey: {\n type: String,\n default: ''\n },\n languages: {\n type: Array as () => TranslatorLanguage[],\n default: () => []\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\n// Use the same SDK instance as the main app\nconst { availableLanguages, currentLanguage, setLanguage, fetchLanguages, lr } = useLangie()\n\n// Use provided languages if available, otherwise use SDK's languages\nconst effectiveLanguages = computed(() => {\n const languages =\n props.languages && props.languages.length > 0 ? props.languages : availableLanguages.value\n\n // Ensure all languages have required fields to prevent jumping\n const processed = languages.map((lang) => ({\n ...lang,\n native_name: lang.native_name || lang.name,\n flag_country: lang.flag_country || lang.code\n }))\n\n // Important: Keep API order (by popularity from language_popularity table)\n // Do NOT sort alphabetically - multiselect may reorder, but we preserve source order\n return processed\n})\n\nconst currentLanguageObject = computed(() => {\n if (!currentLanguage.value) return null\n return effectiveLanguages.value.find((lang) => lang.code === currentLanguage.value) || null\n})\n\n// Function to detect browser language from available languages\nfunction detectBrowserLanguage(languages: TranslatorLanguage[]): string | null {\n if (languages.length === 0) return null\n\n // Get browser languages in order of preference\n const browserLanguages = navigator.languages || [navigator.language || 'en']\n\n // Try to find exact match first\n for (const browserLang of browserLanguages) {\n const langCode = browserLang.toLowerCase().split('-')[0] // Extract language code (e.g., 'en' from 'en-US')\n const exactMatch = languages.find((lang) => lang.code.toLowerCase() === langCode)\n if (exactMatch) {\n return exactMatch.code\n }\n }\n\n // If no exact match, try with full locale (e.g., 'en-US')\n for (const browserLang of browserLanguages) {\n const fullLangCode = browserLang.toLowerCase()\n const localeMatch = languages.find((lang) => lang.code.toLowerCase() === fullLangCode)\n if (localeMatch) {\n return localeMatch.code\n }\n }\n\n return null\n}\n\nasync function handleLanguageChange(selectedLanguage: TranslatorLanguage | null) {\n if (selectedLanguage) {\n // Start loading\n isChangingLanguage.value = true\n\n try {\n setLanguage(selectedLanguage.code)\n // Save to localStorage when user manually changes language\n localStorage.setItem('interface_language', selectedLanguage.code)\n emit('update:modelValue', selectedLanguage)\n } finally {\n // End loading\n isChangingLanguage.value = false\n }\n }\n}\n\n// Watch for current language changes to save to localStorage\nwatch(currentLanguage, (newLangCode) => {\n if (newLangCode) {\n localStorage.setItem('interface_language', newLangCode)\n }\n})\n\n// Watch for changes in effective languages to set browser language\nwatch(\n () => effectiveLanguages.value,\n (newLanguages) => {\n if (newLanguages.length > 0 && !currentLanguage.value) {\n // Only set browser language if no language is currently selected\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode) {\n // Check if saved language exists in the provided languages\n const savedLangExists = newLanguages.find((lang) => lang.code === savedLanguageCode)\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n return\n }\n }\n\n // If no saved language or saved language doesn't exist, detect browser language\n const browserLang = detectBrowserLanguage(newLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n },\n { immediate: true }\n)\n\n// Load saved language from localStorage on initialization\nonMounted(async () => {\n // Only fetch languages if not provided via props\n if (!props.languages || props.languages.length === 0) {\n // Get country code from browser for better language ordering\n const countryCode = await getCountryCode()\n await fetchLanguages({ country: countryCode || undefined })\n }\n\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode && savedLanguageCode !== currentLanguage.value) {\n // Check if saved language exists in current languages\n const currentLanguages = effectiveLanguages.value\n const savedLangExists = currentLanguages.find((lang) => lang.code === savedLanguageCode)\n\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n } else if (currentLanguages.length > 0) {\n // If saved language doesn't exist, try to detect browser language\n const browserLang = detectBrowserLanguage(currentLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n } else if (!currentLanguage.value && effectiveLanguages.value.length > 0) {\n // If no saved language and no current language, detect browser language\n const browserLang = detectBrowserLanguage(effectiveLanguages.value)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n})\n\n// Emit the current language when it changes\nwatch(\n currentLanguageObject,\n (newValue) => {\n if (newValue) {\n emit('update:modelValue', newValue)\n }\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n.interface-language-select-wrapper {\n position: relative;\n display: inline-block;\n width: 100%;\n min-width: 200px;\n}\n\n/* Override styles for the language select component */\n.interface-language-select-wrapper :deep(.multiselect-option) {\n padding: 5px 10px !important;\n margin: 0;\n border-radius: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-option > div) {\n padding: 0 !important;\n margin: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n}\n\n.language-change-loader {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.95);\n border-radius: 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.language-change-loader.is-dark {\n background: rgba(30, 30, 30, 0.95);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.loader-spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top: 3px solid #3b82f6;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-bottom: 8px;\n}\n\n.language-change-loader.is-dark .loader-spinner {\n border-color: #374151;\n border-top-color: #3b82f6;\n}\n\n.loader-text {\n font-size: 12px;\n color: #6b7280;\n font-weight: 500;\n text-align: center;\n}\n\n.language-change-loader.is-dark .loader-text {\n color: #9ca3af;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n</style>\n","<template>\n <transition name=\"fade\" mode=\"out-in\">\n <span :key=\"translated\">{{ translated }}</span>\n </transition>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, useSlots, watch } from 'vue'\nimport { ref, nextTick } from 'vue'\nimport { useLangie } from '../useLangie'\n\n// Define component name for better debugging\n// Component name for Vue devtools\n// Note: defineOptions requires Vue 3.3+, but we support Vue 3.2+\n// The component will work without explicit name\n\n// Detect if we're running in Nuxt\nconst isNuxt = computed(() => {\n if (typeof window !== 'undefined') {\n return !!(window as any).__NUXT__\n }\n if (typeof process !== 'undefined') {\n return !!(process.env as any).NUXT_SSR_BASE || !!(process.env as any).NUXT_PUBLIC_BASE_URL\n }\n return false\n})\n\nconst props = defineProps({\n // Message key (optional, otherwise slot content is used)\n msg: {\n type: String,\n default: undefined\n },\n // Translation context shorthand\n ctx: {\n type: String,\n required: false,\n default: 'ui'\n },\n // Original language shorthand\n orig: {\n type: String,\n required: false,\n default: undefined\n }\n})\n\nconst slots = useSlots()\nconst { lr, currentLanguage, uiTranslations, translations, getLtDefaults } = useLangie()\n\nconst keyStr = computed(() => {\n if (props.msg) return props.msg\n const slotContent = slots.default\n ? slots\n .default()\n .map((n) => n.children)\n .join('')\n : ''\n return (slotContent || '').trim()\n})\n\n// Force component update when translations change\nconst forceUpdate = ref(0)\nwatch(\n [uiTranslations, translations],\n () => {\n // Small delay to ensure translations are cached before updating\n nextTick(() => {\n forceUpdate.value++\n })\n },\n { deep: true }\n)\n\nconst translated = computed(() => {\n // To prevent SSR hydration mismatches in Nuxt, render the untranslated key on the server\n if (isNuxt.value && typeof window === 'undefined') {\n return keyStr.value\n }\n\n // Get global defaults\n const globalDefaults = getLtDefaults()\n\n // Use props or fall back to global defaults\n const effectiveCtx = props.ctx ?? globalDefaults.ctx\n const effectiveOrig = props.orig ?? globalDefaults.orig\n\n // Force reactivity by depending on currentLanguage and translation caches\n void currentLanguage.value\n void forceUpdate.value // Force recomputation when translations change\n // Access properties to create reactive dependencies\n const cacheKey = `${keyStr.value}|${effectiveCtx}`\n const cache = effectiveCtx === 'ui' ? uiTranslations : translations\n void cache[cacheKey] // This creates a reactive dependency\n\n // For other frameworks, always translate\n const result = lr(keyStr.value, effectiveCtx, effectiveOrig)\n return result\n})\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.25s ease;\n}\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["/builds/codelits/langie-sdk/src/components/LanguageSelect.vue","/builds/codelits/langie-sdk/src/components/InterfaceLanguageSelect.vue","/builds/codelits/langie-sdk/src/components/lt.vue"],"sourcesContent":["<template>\n <div class=\"language-select\" :class=\"{ 'is-dark': isDark }\">\n <!-- Show multiselect when we have languages OR when not loading -->\n <Multiselect\n v-if=\"validLanguages.length > 0 || !isLoading\"\n :key=\"multiselectKey\"\n v-model=\"selectedLanguage\"\n :options=\"filteredLanguages\"\n :searchable=\"true\"\n @search-change=\"handleSearch\"\n :can-clear=\"false\"\n :allow-empty=\"false\"\n :object=\"true\"\n :placeholder=\"validLanguages.length === 0 ? 'No languages available' : placeholder\"\n :disabled=\"props.disabled || validLanguages.length === 0\"\n :loading=\"isLoading\"\n track-by=\"name\"\n label=\"name\"\n value-prop=\"code\"\n :filter-results=\"false\"\n @keydown=\"handleKeydown\"\n >\n <template #singlelabel=\"{ value }\">\n <div v-if=\"value\" class=\"multiselect-single-label\">\n <img\n v-if=\"value.code\"\n :key=\"selectedLanguageKey\"\n :src=\"getFlagImageUrl(value)\"\n class=\"lang-flag\"\n :alt=\"`${value.native_name || value.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ value.name }}\n <span class=\"native-name\">({{ value.native_name || value.name }})</span></span\n >\n </div>\n </template>\n <template #option=\"{ option }\">\n <div class=\"multiselect-option\" :data-lang-code=\"option.code\">\n <img\n :key=\"`${option.code}-${option.flag_country || 'fallback'}`\"\n :src=\"getFlagImageUrl(option)\"\n class=\"lang-flag\"\n :alt=\"`${option.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ option.name }}\n <span class=\"native-name\">({{ option.native_name || option.name }})</span></span\n >\n </div>\n </template>\n <template #noresults>\n <div class=\"multiselect-no-results\">No languages found.</div>\n </template>\n <template #nooptions>\n <div class=\"multiselect-no-options\">\n {{\n validLanguages.length === 0\n ? 'Please provide languages via the :languages prop'\n : 'No languages available.'\n }}\n </div>\n </template>\n </Multiselect>\n <!-- Only show skeleton loader when actually loading AND we have no languages yet -->\n <div v-else-if=\"isLoading && validLanguages.length === 0\" class=\"skeleton-loader\"></div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport '@vueform/multiselect/themes/default.css'\nimport '../styles/teleport.css'\nimport { ref, computed, nextTick } from 'vue'\nimport type { PropType } from 'vue'\nimport Multiselect from '@vueform/multiselect'\nimport Fuse from 'fuse.js'\nimport { applyLanguageAlias } from '../search-utils'\nimport { THEME_COLORS, COLORS } from '../constants/colors'\nimport type { TranslatorLanguage } from '../types'\n\nconst getFlagCode = (lang: TranslatorLanguage): string => {\n const flagCode = lang.flag_country || lang.code\n return flagCode\n}\n\nconst getFlagImageUrl = (lang: TranslatorLanguage): string => {\n const flagCode = getFlagCode(lang)\n // Use local flags (downloaded during build for offline support and faster loading)\n // If not found, the onerror handler will use CDN fallback\n return `/flags/${flagCode}.svg`\n}\n\nconst props = defineProps({\n modelValue: {\n type: Object as PropType<TranslatorLanguage | null | undefined>,\n default: undefined\n },\n languages: {\n type: Array as PropType<TranslatorLanguage[]>,\n default: () => []\n },\n placeholder: {\n type: String,\n default: 'Select language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst isLoading = computed(() => {\n return props.languages.length <= 0\n})\nconst searchQuery = ref('')\n\nconst validLanguages = computed(() => {\n return props.languages.filter((lang) => lang && lang.code && lang.name && lang.native_name)\n})\n\nconst fuse = computed(() => {\n if (!validLanguages.value.length) return null\n\n return new Fuse(validLanguages.value, {\n keys: ['name', 'native_name', 'code'],\n includeScore: true,\n threshold: 0.6, // Use highest threshold to catch all cases\n isCaseSensitive: false,\n minMatchCharLength: 1 // Allow single character matches\n })\n})\n\nconst multiselectKey = computed(() => {\n // Create a key that changes when the language data structure changes\n const hasApiData = props.languages.length > 0 && props.languages[0].flag_country !== null\n return hasApiData ? 'api-data' : 'fallback-data'\n})\n\nconst selectedLanguageKey = computed(() => {\n if (!props.modelValue || !props.modelValue.code) return 'no-selection'\n const key = `${props.modelValue.code}-${props.modelValue.flag_country || 'fallback'}`\n return key\n})\n\nconst selectedLanguage = computed({\n get: () => props.modelValue || null,\n set: (value) => {\n if (value && value.code) {\n emit('update:modelValue', value)\n }\n }\n})\n\nconst filteredLanguages = computed(() => {\n const query = searchQuery.value.trim()\n\n let results: TranslatorLanguage[]\n if (!query) {\n // When no search query, return all valid languages (bypass Fuse.js)\n results = validLanguages.value\n } else {\n // Only use Fuse.js when there's an actual search query\n if (!fuse.value) return []\n\n // Get the most likely alias or the original term\n const aliasResult = applyLanguageAlias(query)\n const searchTerm = Array.isArray(aliasResult.primary)\n ? aliasResult.primary[0]\n : aliasResult.primary\n\n // Always use Fuse.js for the main search (both original query and alias result)\n const fuseResults = fuse.value.search(searchTerm)\n results = fuseResults.map((result) => result.item)\n\n // If the search term is different from query (alias was applied), also search the original query\n if (searchTerm !== query) {\n const originalResults = fuse.value.search(query)\n originalResults.forEach((result) => {\n if (!results.some((r) => r.code === result.item.code)) {\n results.push(result.item)\n }\n })\n }\n\n // Add suggested languages to expand the results\n if (aliasResult.suggestions.length > 0) {\n aliasResult.suggestions.forEach((suggestion) => {\n const suggestedLang = validLanguages.value.find((lang) => {\n const langName = lang.name.toLowerCase()\n const suggestion_lower = suggestion.toLowerCase()\n\n // Try multiple matching strategies\n return (\n langName === suggestion_lower || // Exact match\n langName.includes(suggestion_lower) || // Contains match\n lang.code.toLowerCase() === suggestion_lower || // Code match\n langName.startsWith(suggestion_lower) || // Starts with match\n // Handle common variations\n (suggestion_lower === 'tatar' && langName.includes('tatar')) ||\n (suggestion_lower === 'belarusian' &&\n (langName.includes('belarus') || langName.includes('belarusian'))) ||\n (suggestion_lower === 'moldovan' &&\n (langName.includes('moldov') || langName.includes('moldova')))\n )\n })\n if (suggestedLang && !results.some((r) => r.code === suggestedLang.code)) {\n results.push(suggestedLang)\n }\n })\n }\n\n // Special handling for very short queries (e.g. \"се\")\n if (query.length === 2) {\n const lowerQuery = query.toLowerCase()\n const manualResults = validLanguages.value.filter(\n (lang) =>\n lang.name.toLowerCase().startsWith(lowerQuery) ||\n lang.native_name.toLowerCase().startsWith(lowerQuery)\n )\n manualResults.forEach((lang) => {\n if (!results.some((r) => r.code === lang.code)) {\n results.push(lang)\n }\n })\n }\n }\n\n // Remove the currently selected language from the options\n const filtered = results.filter(\n (lang) => !props.modelValue || !props.modelValue.code || lang.code !== props.modelValue.code\n )\n\n return filtered\n})\n\nfunction handleSearch(query: string) {\n searchQuery.value = query\n\n // Auto-scroll to first result when typing\n if (query.trim()) {\n nextTick(() => {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption) {\n firstOption.scrollIntoView({ behavior: 'smooth', block: 'nearest' })\n // Highlight the first option\n firstOption.classList.add('multiselect-option-is-pointed')\n }\n })\n }\n}\n\n// Handle keyboard navigation\nfunction handleKeydown(event: Event) {\n const keyboardEvent = event as KeyboardEvent\n if (keyboardEvent.key === 'Tab' || keyboardEvent.key === 'Enter') {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption && searchQuery.value.trim()) {\n // Get the language data from the first option\n const langCode = firstOption.getAttribute('data-lang-code')\n if (langCode) {\n const language = validLanguages.value.find((lang) => lang.code === langCode)\n if (language) {\n selectedLanguage.value = language\n searchQuery.value = ''\n keyboardEvent.preventDefault()\n }\n }\n }\n }\n}\n\nconst onFlagError = (event: Event) => {\n const target = event.target as HTMLImageElement\n const currentSrc = target.src\n\n // If loading from local cache failed, try CDN\n if (currentSrc && currentSrc.includes('/flags/')) {\n const match = currentSrc.match(/\\/flags\\/([a-z]{2})\\.svg/)\n if (match) {\n const countryCode = match[1]\n // Fallback to CDN\n target.src = `https://flagcdn.com/${countryCode}.svg`\n // If CDN also fails, hide the image\n target.onerror = () => {\n target.style.display = 'none'\n }\n return\n }\n }\n\n // If CDN or any other source fails, hide the image\n target.style.display = 'none'\n}\n</script>\n\n<style scoped>\n.language-select {\n position: relative;\n min-height: 40px; /* To prevent layout shift while loading */\n}\n\n.skeleton-loader {\n width: 100%;\n height: 40px;\n background-color: #e2e8f0;\n border-radius: 4px;\n animation: pulse 1.5s infinite ease-in-out;\n}\n\n.is-dark .skeleton-loader {\n background-color: #374151;\n}\n\n@keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n:deep(.multiselect) {\n --ms-font-size: 1rem;\n --ms-line-height: 1.5;\n --ms-bg: v-bind('THEME_COLORS.light.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.light.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.light.border');\n --ms-border-width: 1px;\n --ms-radius: 4px;\n --ms-py: 0.3rem;\n --ms-px: 0.875rem;\n --ms-ring-width: 3px;\n --ms-ring-color: v-bind('THEME_COLORS.light.ring');\n --ms-placeholder-color: v-bind('THEME_COLORS.light.placeholder');\n --ms-color: #1f2937; /* Explicit text color for light theme */\n\n /* Options */\n --ms-option-font-size: 1rem;\n --ms-option-line-height: 1;\n --ms-option-py: 0.2rem;\n --ms-option-px: 0.5rem;\n --ms-option-bg-pointed: v-bind('THEME_COLORS.light.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.light.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.light.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.light.optionSelectedText');\n --ms-option-bg-disabled: #fff;\n --ms-option-color-disabled: #d1d5db;\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.light.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.light.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.light.dropdownText');\n\n /* Others */\n --ms-tag-bg: v-bind('THEME_COLORS.light.tagBackground');\n}\n\n.is-dark :deep(.multiselect) {\n --ms-bg: v-bind('THEME_COLORS.dark.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.dark.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.dark.border');\n --ms-placeholder-color: v-bind('THEME_COLORS.dark.placeholder');\n --ms-ring-color: v-bind('THEME_COLORS.dark.ring');\n\n /* Options */\n --ms-option-bg-pointed: v-bind('THEME_COLORS.dark.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.dark.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.dark.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.dark.optionSelectedText');\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.dark.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.dark.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.lang-flag) {\n width: 20px;\n height: 15px;\n border: 1px solid v-bind('COLORS.border.flag');\n border-radius: 2px;\n object-fit: cover;\n flex-shrink: 0;\n display: block;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n\n.is-dark :deep(.lang-flag) {\n border-color: v-bind('COLORS.border.dark');\n}\n\n:deep(.multiselect-single-label),\n:deep(.multiselect-option) {\n display: flex !important;\n align-items: center !important;\n min-width: 0 !important;\n width: 100% !important;\n padding: 4px 8px !important;\n gap: 8px !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-option) {\n text-align: left !important;\n transition: background-color 0.15s ease !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.language-text) {\n display: inline-block !important;\n min-width: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n flex: 1 !important;\n line-height: 1.4 !important;\n color: v-bind('THEME_COLORS.light.dropdownText') !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n.is-dark :deep(.language-text) {\n color: v-bind('THEME_COLORS.dark.dropdownText') !important;\n}\n\n:deep(.native-name) {\n color: v-bind('COLORS.text.secondary');\n margin-left: 0.5rem;\n font-size: 0.9em;\n white-space: nowrap !important;\n display: inline !important;\n}\n\n.is-dark :deep(.native-name) {\n color: v-bind('COLORS.neutral.gray400');\n}\n\n:deep(.multiselect) {\n width: 100%;\n min-width: 200px;\n color: v-bind('THEME_COLORS.light.dropdownText');\n position: relative;\n z-index: 1;\n}\n\n.is-dark :deep(.multiselect) {\n color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n z-index: 1000 !important;\n}\n\n:deep(.multiselect-dropdown .multiselect-option) {\n padding: 4px 8px !important;\n margin: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-dropdown .language-text) {\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n:deep(.multiselect-no-results),\n:deep(.multiselect-no-options) {\n padding: 0.5rem 0.875rem;\n color: v-bind('COLORS.text.secondary');\n}\n\n.is-dark :deep(.multiselect-no-results),\n.is-dark :deep(.multiselect-no-options) {\n color: v-bind('COLORS.neutral.gray400');\n}\n</style>\n","<template>\n <div class=\"interface-language-select-wrapper\">\n <LanguageSelect\n :key=\"`interface-lang-${effectiveLanguages.length}-${effectiveLanguages[0]?.code || 'empty'}`\"\n :model-value=\"currentLanguageObject\"\n :languages=\"effectiveLanguages\"\n :placeholder=\"props.placeholder\"\n :disabled=\"props.disabled || isChangingLanguage\"\n :is-dark=\"props.isDark\"\n @update:model-value=\"handleLanguageChange\"\n />\n\n <!-- Loading overlay -->\n <div\n v-if=\"isChangingLanguage\"\n class=\"language-change-loader\"\n :class=\"{ 'is-dark': props.isDark }\"\n >\n <div class=\"loader-spinner\"></div>\n <span class=\"loader-text\">{{ lr('Changing language...', 'ui') }}</span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, watch, onMounted, ref } from 'vue'\nimport LanguageSelect from './LanguageSelect.vue'\nimport { useLangie } from '../useLangie'\nimport type { TranslatorLanguage } from '../types'\nimport { getCountryCode } from '../utils/getCountryCode'\n\n// Loading state for language change\nconst isChangingLanguage = ref(false)\n\nconst props = defineProps({\n placeholder: {\n type: String,\n default: 'Select interface language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n },\n translatorHost: {\n type: String,\n default: ''\n },\n apiKey: {\n type: String,\n default: ''\n },\n languages: {\n type: Array as () => TranslatorLanguage[],\n default: () => []\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\n// Use the same SDK instance as the main app\nconst { availableLanguages, currentLanguage, setLanguage, fetchLanguages, lr } = useLangie()\n\n// Use provided languages if available, otherwise use SDK's languages\nconst effectiveLanguages = computed(() => {\n const languages =\n props.languages && props.languages.length > 0 ? props.languages : availableLanguages.value\n\n // Ensure all languages have required fields to prevent jumping\n const processed = languages.map((lang) => ({\n ...lang,\n native_name: lang.native_name || lang.name,\n flag_country: lang.flag_country || lang.code\n }))\n\n // Important: Keep API order (by popularity from language_popularity table)\n // Do NOT sort alphabetically - multiselect may reorder, but we preserve source order\n return processed\n})\n\nconst currentLanguageObject = computed(() => {\n if (!currentLanguage.value) return null\n return effectiveLanguages.value.find((lang) => lang.code === currentLanguage.value) || null\n})\n\n// Function to detect browser language from available languages\nfunction detectBrowserLanguage(languages: TranslatorLanguage[]): string | null {\n if (languages.length === 0) return null\n\n // Get browser languages in order of preference\n const browserLanguages = navigator.languages || [navigator.language || 'en']\n\n // Try to find exact match first\n for (const browserLang of browserLanguages) {\n const langCode = browserLang.toLowerCase().split('-')[0] // Extract language code (e.g., 'en' from 'en-US')\n const exactMatch = languages.find((lang) => lang.code.toLowerCase() === langCode)\n if (exactMatch) {\n return exactMatch.code\n }\n }\n\n // If no exact match, try with full locale (e.g., 'en-US')\n for (const browserLang of browserLanguages) {\n const fullLangCode = browserLang.toLowerCase()\n const localeMatch = languages.find((lang) => lang.code.toLowerCase() === fullLangCode)\n if (localeMatch) {\n return localeMatch.code\n }\n }\n\n return null\n}\n\nasync function handleLanguageChange(selectedLanguage: TranslatorLanguage | null) {\n if (selectedLanguage) {\n // Start loading\n isChangingLanguage.value = true\n\n try {\n setLanguage(selectedLanguage.code)\n // Save to localStorage when user manually changes language\n localStorage.setItem('interface_language', selectedLanguage.code)\n emit('update:modelValue', selectedLanguage)\n } finally {\n // End loading\n isChangingLanguage.value = false\n }\n }\n}\n\n// Watch for current language changes to save to localStorage\nwatch(currentLanguage, (newLangCode) => {\n if (newLangCode) {\n localStorage.setItem('interface_language', newLangCode)\n }\n})\n\n// Watch for changes in effective languages to set browser language\nwatch(\n () => effectiveLanguages.value,\n (newLanguages) => {\n if (newLanguages.length > 0 && !currentLanguage.value) {\n // Only set browser language if no language is currently selected\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode) {\n // Check if saved language exists in the provided languages\n const savedLangExists = newLanguages.find((lang) => lang.code === savedLanguageCode)\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n return\n }\n }\n\n // If no saved language or saved language doesn't exist, detect browser language\n const browserLang = detectBrowserLanguage(newLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n },\n { immediate: true }\n)\n\n// Load saved language from localStorage on initialization\nonMounted(async () => {\n // Only fetch languages if not provided via props\n if (!props.languages || props.languages.length === 0) {\n // Get country code from browser for better language ordering\n const countryCode = await getCountryCode()\n await fetchLanguages({ country: countryCode || undefined })\n }\n\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode && savedLanguageCode !== currentLanguage.value) {\n // Check if saved language exists in current languages\n const currentLanguages = effectiveLanguages.value\n const savedLangExists = currentLanguages.find((lang) => lang.code === savedLanguageCode)\n\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n } else if (currentLanguages.length > 0) {\n // If saved language doesn't exist, try to detect browser language\n const browserLang = detectBrowserLanguage(currentLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n } else if (!currentLanguage.value && effectiveLanguages.value.length > 0) {\n // If no saved language and no current language, detect browser language\n const browserLang = detectBrowserLanguage(effectiveLanguages.value)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n})\n\n// Emit the current language when it changes\nwatch(\n currentLanguageObject,\n (newValue) => {\n if (newValue) {\n emit('update:modelValue', newValue)\n }\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n.interface-language-select-wrapper {\n position: relative;\n display: inline-block;\n width: 100%;\n min-width: 200px;\n}\n\n/* Override styles for the language select component */\n.interface-language-select-wrapper :deep(.multiselect-option) {\n padding: 5px 10px !important;\n margin: 0;\n border-radius: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-option > div) {\n padding: 0 !important;\n margin: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n}\n\n.language-change-loader {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.95);\n border-radius: 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.language-change-loader.is-dark {\n background: rgba(30, 30, 30, 0.95);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.loader-spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top: 3px solid #3b82f6;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-bottom: 8px;\n}\n\n.language-change-loader.is-dark .loader-spinner {\n border-color: #374151;\n border-top-color: #3b82f6;\n}\n\n.loader-text {\n font-size: 12px;\n color: #6b7280;\n font-weight: 500;\n text-align: center;\n}\n\n.language-change-loader.is-dark .loader-text {\n color: #9ca3af;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n</style>\n","<template>\n <transition name=\"fade\" mode=\"out-in\">\n <span :key=\"translated\">{{ translated }}</span>\n </transition>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, useSlots, watch } from 'vue'\nimport { ref, nextTick } from 'vue'\nimport { useLangie } from '../useLangie'\n\n// Define component name for better debugging\n// Component name for Vue devtools\n// Note: defineOptions requires Vue 3.3+, but we support Vue 3.2+\n// The component will work without explicit name\n\n// Detect if we're running in Nuxt\nconst isNuxt = computed(() => {\n if (typeof window !== 'undefined') {\n return !!(window as any).__NUXT__\n }\n if (typeof process !== 'undefined') {\n return !!(process.env as any).NUXT_SSR_BASE || !!(process.env as any).NUXT_PUBLIC_BASE_URL\n }\n return false\n})\n\nconst props = defineProps({\n // Message key (optional, otherwise slot content is used)\n msg: {\n type: String,\n default: undefined\n },\n // Translation context shorthand\n ctx: {\n type: String,\n required: false,\n default: 'ui'\n },\n // Original language shorthand\n orig: {\n type: String,\n required: false,\n default: undefined\n }\n})\n\nconst slots = useSlots()\nconst { lr, currentLanguage, uiTranslations, translations, getLtDefaults } = useLangie()\n\nconst keyStr = computed(() => {\n if (props.msg) return props.msg\n const slotContent = slots.default\n ? slots\n .default()\n .map((n) => n.children)\n .join('')\n : ''\n return (slotContent || '').trim()\n})\n\n// Force component update when translations change\nconst forceUpdate = ref(0)\nwatch(\n [uiTranslations, translations],\n () => {\n // Small delay to ensure translations are cached before updating\n nextTick(() => {\n forceUpdate.value++\n })\n },\n { deep: true }\n)\n\nconst translated = computed(() => {\n // To prevent SSR hydration mismatches in Nuxt, render the untranslated key on the server\n if (isNuxt.value && typeof window === 'undefined') {\n return keyStr.value\n }\n\n // Get global defaults\n const globalDefaults = getLtDefaults()\n\n // Use props or fall back to global defaults\n const effectiveCtx = props.ctx ?? globalDefaults.ctx\n const effectiveOrig = props.orig ?? globalDefaults.orig\n\n // Force reactivity by depending on currentLanguage and translation caches\n void currentLanguage.value\n void forceUpdate.value // Force recomputation when translations change\n // Access properties to create reactive dependencies\n const cacheKey = `${keyStr.value}|${effectiveCtx}`\n const cache = effectiveCtx === 'ui' ? uiTranslations : translations\n void cache[cacheKey] // This creates a reactive dependency\n\n // For other frameworks, always translate\n const result = lr(keyStr.value, effectiveCtx, effectiveOrig)\n return result\n})\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.25s ease;\n}\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n"],"mappings":"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","names":[]}
|