@pure-ds/core 0.7.38 → 0.7.39
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/custom-elements.json +77 -4
- package/dist/types/public/assets/pds/components/pds-locale.d.ts +10 -0
- package/dist/types/public/assets/pds/components/pds-locale.d.ts.map +1 -0
- package/package.json +1 -1
- package/public/assets/pds/components/pds-live-edit.js +10 -458
- package/public/assets/pds/components/pds-locale.js +1132 -0
- package/public/assets/pds/vscode-custom-data.json +10 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
import { PDS, msg } from "#pds";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<pds-locale>` renders a locale switcher backed by configured/runtime
|
|
5
|
+
* localization locales and keeps its checked state synced with `<html lang>`.
|
|
6
|
+
*
|
|
7
|
+
* @element pds-locale
|
|
8
|
+
*/
|
|
9
|
+
const LAYERS = ["tokens", "primitives", "components", "utilities"];
|
|
10
|
+
|
|
11
|
+
let startupLocalizationLocales = null;
|
|
12
|
+
let startupLocalizationLocalesPromise = null;
|
|
13
|
+
|
|
14
|
+
const LOCALE_PROBE_CANDIDATES = [
|
|
15
|
+
"en", "nl", "de", "fr", "es", "it", "pt", "sv", "no", "da", "fi",
|
|
16
|
+
"pl", "cs", "sk", "sl", "hu", "ro", "bg", "hr", "sr", "ru", "uk",
|
|
17
|
+
"tr", "el", "he", "ar", "fa", "hi", "ja", "ko", "zh", "zh-cn", "zh-tw",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function normalizeLocaleTag(locale) {
|
|
21
|
+
return String(locale || "").trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toCanonicalLocaleTag(locale) {
|
|
25
|
+
const normalized = normalizeLocaleTag(locale);
|
|
26
|
+
if (!normalized) return "";
|
|
27
|
+
|
|
28
|
+
if (typeof Intl !== "undefined" && typeof Intl.getCanonicalLocales === "function") {
|
|
29
|
+
try {
|
|
30
|
+
const [canonical] = Intl.getCanonicalLocales(normalized);
|
|
31
|
+
if (canonical) {
|
|
32
|
+
return canonical;
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isFiveLetterLocaleTag(locale) {
|
|
41
|
+
return /^[a-z]{2}-[A-Z]{2}$/.test(String(locale || ""));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectKnownFiveLetterLocales() {
|
|
45
|
+
const known = new Set();
|
|
46
|
+
|
|
47
|
+
const add = (value) => {
|
|
48
|
+
const canonical = toCanonicalLocaleTag(value);
|
|
49
|
+
if (isFiveLetterLocaleTag(canonical)) {
|
|
50
|
+
known.add(canonical);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const configLocalization =
|
|
55
|
+
PDS?.currentConfig?.localization && typeof PDS.currentConfig.localization === "object"
|
|
56
|
+
? PDS.currentConfig.localization
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
(configLocalization?.locales || []).forEach(add);
|
|
60
|
+
(configLocalization?.provider?.locales || []).forEach(add);
|
|
61
|
+
|
|
62
|
+
const runtimeState =
|
|
63
|
+
typeof PDS?.getLocalizationState === "function"
|
|
64
|
+
? PDS.getLocalizationState()
|
|
65
|
+
: null;
|
|
66
|
+
(runtimeState?.loadedLocales || []).forEach(add);
|
|
67
|
+
|
|
68
|
+
return Array.from(known);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveFiveLetterLocaleTag(locale, knownLocales = []) {
|
|
72
|
+
const canonical = toCanonicalLocaleTag(locale);
|
|
73
|
+
if (isFiveLetterLocaleTag(canonical)) {
|
|
74
|
+
return canonical;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const base = toBaseLocale(canonical);
|
|
78
|
+
if (!base) return "";
|
|
79
|
+
|
|
80
|
+
const mapped = knownLocales.find((candidate) => toBaseLocale(candidate) === base);
|
|
81
|
+
return mapped || "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toBaseLocale(locale) {
|
|
85
|
+
const normalized = normalizeLocaleTag(locale);
|
|
86
|
+
if (!normalized) return "";
|
|
87
|
+
return normalized.split("-")[0] || normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isLocalizationActive() {
|
|
91
|
+
const configLocalization =
|
|
92
|
+
PDS?.currentConfig?.localization && typeof PDS.currentConfig.localization === "object"
|
|
93
|
+
? PDS.currentConfig.localization
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
const runtimeState =
|
|
97
|
+
typeof PDS?.getLocalizationState === "function"
|
|
98
|
+
? PDS.getLocalizationState()
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
const hasRuntimeProvider = Boolean(runtimeState?.hasProvider);
|
|
102
|
+
|
|
103
|
+
const hasConfigProvider = Boolean(
|
|
104
|
+
configLocalization?.provider ||
|
|
105
|
+
typeof configLocalization?.translate === "function" ||
|
|
106
|
+
typeof configLocalization?.loadLocale === "function" ||
|
|
107
|
+
typeof configLocalization?.setLocale === "function"
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const hasConfigMessages = Boolean(
|
|
111
|
+
configLocalization?.messages &&
|
|
112
|
+
typeof configLocalization.messages === "object" &&
|
|
113
|
+
Object.keys(configLocalization.messages).length > 0
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const hasRuntimeMessages = Boolean(
|
|
117
|
+
runtimeState?.messages &&
|
|
118
|
+
typeof runtimeState.messages === "object" &&
|
|
119
|
+
Object.keys(runtimeState.messages).length > 0
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return hasRuntimeProvider || hasConfigProvider || hasConfigMessages || hasRuntimeMessages;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isLocaleBundle(bundle) {
|
|
126
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Object.values(bundle).some((value) => {
|
|
131
|
+
if (typeof value === "string") return true;
|
|
132
|
+
return Boolean(value && typeof value === "object" && typeof value.content === "string");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeMessageBundle(bundle) {
|
|
137
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const normalized = {};
|
|
142
|
+
Object.entries(bundle).forEach(([key, value]) => {
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
normalized[key] = value;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (value && typeof value === "object" && typeof value.content === "string") {
|
|
149
|
+
normalized[key] = value.content;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getLocalizationProviderLoader(configLocalization) {
|
|
157
|
+
if (!configLocalization || typeof configLocalization !== "object") return null;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
(typeof configLocalization?.loadLocale === "function"
|
|
161
|
+
? configLocalization.loadLocale
|
|
162
|
+
: null) ||
|
|
163
|
+
(typeof configLocalization?.provider?.loadLocale === "function"
|
|
164
|
+
? configLocalization.provider.loadLocale
|
|
165
|
+
: null) ||
|
|
166
|
+
(typeof configLocalization?.setLocale === "function"
|
|
167
|
+
? configLocalization.setLocale
|
|
168
|
+
: null) ||
|
|
169
|
+
(typeof configLocalization?.provider?.setLocale === "function"
|
|
170
|
+
? configLocalization.provider.setLocale
|
|
171
|
+
: null)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildLocaleProbeList({ defaultLocale, runtimeState, knownLocales }) {
|
|
176
|
+
const candidates = new Set();
|
|
177
|
+
|
|
178
|
+
const normalizedDefault = normalizeLocaleTag(defaultLocale);
|
|
179
|
+
if (normalizedDefault) {
|
|
180
|
+
candidates.add(normalizedDefault);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Array.isArray(runtimeState?.loadedLocales)) {
|
|
184
|
+
runtimeState.loadedLocales.forEach((locale) => {
|
|
185
|
+
const normalized = normalizeLocaleTag(locale);
|
|
186
|
+
if (normalized) {
|
|
187
|
+
candidates.add(normalized);
|
|
188
|
+
candidates.add(toBaseLocale(normalized));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (Array.isArray(knownLocales)) {
|
|
194
|
+
knownLocales.forEach((locale) => {
|
|
195
|
+
const normalized = normalizeLocaleTag(locale);
|
|
196
|
+
if (normalized) {
|
|
197
|
+
candidates.add(normalized);
|
|
198
|
+
candidates.add(toBaseLocale(normalized));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (typeof navigator !== "undefined" && Array.isArray(navigator.languages)) {
|
|
204
|
+
navigator.languages.forEach((locale) => {
|
|
205
|
+
const normalized = normalizeLocaleTag(locale);
|
|
206
|
+
if (normalized) {
|
|
207
|
+
candidates.add(normalized);
|
|
208
|
+
candidates.add(toBaseLocale(normalized));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
LOCALE_PROBE_CANDIDATES.forEach((locale) => {
|
|
214
|
+
const normalized = normalizeLocaleTag(locale);
|
|
215
|
+
if (normalized) {
|
|
216
|
+
candidates.add(normalized);
|
|
217
|
+
candidates.add(toBaseLocale(normalized));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
candidates.delete("");
|
|
222
|
+
return Array.from(candidates).sort((a, b) => a.localeCompare(b));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function bundlesDifferFromOrigin(originBundle, candidateBundle) {
|
|
226
|
+
const originEntries = normalizeMessageBundle(originBundle);
|
|
227
|
+
const candidateEntries = normalizeMessageBundle(candidateBundle);
|
|
228
|
+
|
|
229
|
+
const originKeys = Object.keys(originEntries);
|
|
230
|
+
const candidateKeys = Object.keys(candidateEntries);
|
|
231
|
+
if (!originKeys.length || !candidateKeys.length) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return originKeys.some((key) => {
|
|
236
|
+
if (!Object.prototype.hasOwnProperty.call(candidateEntries, key)) return false;
|
|
237
|
+
return String(candidateEntries[key]) !== String(originEntries[key]);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function collectLocalesFromMessageRows(source, locales) {
|
|
242
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return;
|
|
243
|
+
|
|
244
|
+
Object.values(source).forEach((row) => {
|
|
245
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return;
|
|
246
|
+
if (typeof row.content === "string") return;
|
|
247
|
+
|
|
248
|
+
Object.entries(row).forEach(([localeKey, translatedValue]) => {
|
|
249
|
+
const normalized = normalizeLocaleTag(localeKey);
|
|
250
|
+
if (!normalized) return;
|
|
251
|
+
|
|
252
|
+
const hasValue =
|
|
253
|
+
typeof translatedValue === "string" ||
|
|
254
|
+
Boolean(
|
|
255
|
+
translatedValue &&
|
|
256
|
+
typeof translatedValue === "object" &&
|
|
257
|
+
typeof translatedValue.content === "string"
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (hasValue) {
|
|
261
|
+
locales.add(normalized);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collectLocalesFromLocaleBundles(source, locales) {
|
|
268
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return;
|
|
269
|
+
|
|
270
|
+
Object.entries(source).forEach(([localeKey, bundle]) => {
|
|
271
|
+
const normalized = normalizeLocaleTag(localeKey);
|
|
272
|
+
if (!normalized) return;
|
|
273
|
+
if (!isLocaleBundle(bundle)) return;
|
|
274
|
+
locales.add(normalized);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function collectLocalesFromConfiguredList(source, locales) {
|
|
279
|
+
if (!Array.isArray(source)) return;
|
|
280
|
+
|
|
281
|
+
source.forEach((localeValue) => {
|
|
282
|
+
const normalized = normalizeLocaleTag(localeValue);
|
|
283
|
+
if (normalized) {
|
|
284
|
+
locales.add(normalized);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function detectStartupLocalizationLocales() {
|
|
290
|
+
const locales = new Set();
|
|
291
|
+
|
|
292
|
+
const configLocalization =
|
|
293
|
+
PDS?.currentConfig?.localization && typeof PDS.currentConfig.localization === "object"
|
|
294
|
+
? PDS.currentConfig.localization
|
|
295
|
+
: null;
|
|
296
|
+
|
|
297
|
+
const defaultLocale = normalizeLocaleTag(configLocalization?.locale);
|
|
298
|
+
if (defaultLocale) {
|
|
299
|
+
locales.add(defaultLocale);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
collectLocalesFromConfiguredList(configLocalization?.locales, locales);
|
|
303
|
+
collectLocalesFromConfiguredList(configLocalization?.provider?.locales, locales);
|
|
304
|
+
|
|
305
|
+
if (locales.size >= 2) {
|
|
306
|
+
return Array.from(locales).sort((a, b) => a.localeCompare(b));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const runtimeState =
|
|
310
|
+
typeof PDS?.getLocalizationState === "function"
|
|
311
|
+
? PDS.getLocalizationState()
|
|
312
|
+
: null;
|
|
313
|
+
const runtimeDefaultLocale = normalizeLocaleTag(runtimeState?.locale);
|
|
314
|
+
if (runtimeDefaultLocale) {
|
|
315
|
+
locales.add(runtimeDefaultLocale);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const messageSources = [
|
|
319
|
+
configLocalization?.messages,
|
|
320
|
+
configLocalization?.messagesByLocale,
|
|
321
|
+
configLocalization?.i18n,
|
|
322
|
+
configLocalization?.translations,
|
|
323
|
+
configLocalization?.provider?.messages,
|
|
324
|
+
configLocalization?.provider?.messagesByLocale,
|
|
325
|
+
configLocalization?.provider?.i18n,
|
|
326
|
+
configLocalization?.provider?.translations,
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
messageSources.forEach((source) => {
|
|
330
|
+
collectLocalesFromMessageRows(source, locales);
|
|
331
|
+
collectLocalesFromLocaleBundles(source, locales);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const localeListFromRows = Array.from(locales).sort((a, b) => a.localeCompare(b));
|
|
335
|
+
if (localeListFromRows.length >= 2) {
|
|
336
|
+
return localeListFromRows;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const providerLoader = getLocalizationProviderLoader(configLocalization);
|
|
340
|
+
const runtimeLoadLocale =
|
|
341
|
+
typeof PDS?.loadLocale === "function" ? PDS.loadLocale.bind(PDS) : null;
|
|
342
|
+
|
|
343
|
+
if (typeof providerLoader !== "function" && typeof runtimeLoadLocale !== "function") {
|
|
344
|
+
return localeListFromRows;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const originLocale =
|
|
348
|
+
normalizeLocaleTag(configLocalization?.locale) ||
|
|
349
|
+
normalizeLocaleTag(runtimeState?.locale) ||
|
|
350
|
+
"en";
|
|
351
|
+
|
|
352
|
+
locales.add(originLocale);
|
|
353
|
+
|
|
354
|
+
let originBundle = normalizeMessageBundle(runtimeState?.messages || configLocalization?.messages);
|
|
355
|
+
if (!Object.keys(originBundle).length) {
|
|
356
|
+
try {
|
|
357
|
+
let loadedOrigin = null;
|
|
358
|
+
|
|
359
|
+
if (typeof runtimeLoadLocale === "function") {
|
|
360
|
+
loadedOrigin = await Promise.resolve(runtimeLoadLocale(originLocale));
|
|
361
|
+
} else {
|
|
362
|
+
loadedOrigin = await Promise.resolve(
|
|
363
|
+
providerLoader({
|
|
364
|
+
locale: originLocale,
|
|
365
|
+
defaultLocale: originLocale,
|
|
366
|
+
reason: "startup-locale-detect-origin",
|
|
367
|
+
loadedLocales: Array.from(locales),
|
|
368
|
+
messages: {},
|
|
369
|
+
load: true,
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
originBundle = normalizeMessageBundle(loadedOrigin);
|
|
375
|
+
} catch (error) {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const probeLocales = buildLocaleProbeList({
|
|
379
|
+
defaultLocale: originLocale,
|
|
380
|
+
runtimeState,
|
|
381
|
+
knownLocales: Array.from(locales),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
for (const candidateLocale of probeLocales) {
|
|
385
|
+
if (!candidateLocale || candidateLocale === originLocale) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
let candidateBundle = null;
|
|
391
|
+
|
|
392
|
+
if (typeof runtimeLoadLocale === "function") {
|
|
393
|
+
candidateBundle = await Promise.resolve(runtimeLoadLocale(candidateLocale));
|
|
394
|
+
} else {
|
|
395
|
+
candidateBundle = await Promise.resolve(
|
|
396
|
+
providerLoader({
|
|
397
|
+
locale: candidateLocale,
|
|
398
|
+
defaultLocale: originLocale,
|
|
399
|
+
reason: "startup-locale-detect-probe",
|
|
400
|
+
loadedLocales: Array.from(locales),
|
|
401
|
+
messages: {},
|
|
402
|
+
load: true,
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (bundlesDifferFromOrigin(originBundle, candidateBundle)) {
|
|
408
|
+
locales.add(candidateLocale);
|
|
409
|
+
if (locales.size >= 2) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return Array.from(locales).sort((a, b) => a.localeCompare(b));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function getStartupLocalizationLocales({ forceReload = false } = {}) {
|
|
420
|
+
if (forceReload) {
|
|
421
|
+
startupLocalizationLocales = null;
|
|
422
|
+
startupLocalizationLocalesPromise = null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (Array.isArray(startupLocalizationLocales)) {
|
|
426
|
+
return [...startupLocalizationLocales];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (startupLocalizationLocalesPromise) {
|
|
430
|
+
const inFlight = await startupLocalizationLocalesPromise;
|
|
431
|
+
return [...inFlight];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
startupLocalizationLocalesPromise = detectStartupLocalizationLocales()
|
|
435
|
+
.then((locales) => {
|
|
436
|
+
startupLocalizationLocales = Array.isArray(locales) ? locales : [];
|
|
437
|
+
return startupLocalizationLocales;
|
|
438
|
+
})
|
|
439
|
+
.catch(() => {
|
|
440
|
+
startupLocalizationLocales = [];
|
|
441
|
+
return startupLocalizationLocales;
|
|
442
|
+
})
|
|
443
|
+
.finally(() => {
|
|
444
|
+
startupLocalizationLocalesPromise = null;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const resolved = await startupLocalizationLocalesPromise;
|
|
448
|
+
return [...resolved];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function localeOptionLabel(locale) {
|
|
452
|
+
const canonical = toCanonicalLocaleTag(locale);
|
|
453
|
+
const normalized = normalizeLocaleTag(canonical);
|
|
454
|
+
if (!normalized) return "";
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
// Use the locale's own language for its label (self-localized).
|
|
458
|
+
const displayNames = new Intl.DisplayNames([canonical], {
|
|
459
|
+
type: "language",
|
|
460
|
+
});
|
|
461
|
+
const named = displayNames.of(canonical);
|
|
462
|
+
if (named && String(named).trim()) {
|
|
463
|
+
return named;
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {}
|
|
466
|
+
|
|
467
|
+
return canonical || normalized;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function localeOptionAlias(locale) {
|
|
471
|
+
const normalized = normalizeLocaleTag(locale);
|
|
472
|
+
if (!normalized) return "";
|
|
473
|
+
|
|
474
|
+
const base = toBaseLocale(normalized);
|
|
475
|
+
if (!base) return normalized;
|
|
476
|
+
return base.slice(0, 2);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function localeMatches(selectedLocale, activeLocale) {
|
|
480
|
+
const selected = normalizeLocaleTag(selectedLocale);
|
|
481
|
+
const active = normalizeLocaleTag(activeLocale);
|
|
482
|
+
if (!selected || !active) return false;
|
|
483
|
+
return selected === active || toBaseLocale(selected) === toBaseLocale(active);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Payload for `pds:locale:changed`.
|
|
488
|
+
*
|
|
489
|
+
* @typedef {Object} PdsLocaleChangedDetail
|
|
490
|
+
* @property {string} locale Canonical 5-letter locale tag (`xx-YY`).
|
|
491
|
+
*/
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Locale switcher component.
|
|
495
|
+
*
|
|
496
|
+
* The component only persists canonical 5-letter locale tags (`xx-YY`),
|
|
497
|
+
* for example `en-US` and `nl-NL`.
|
|
498
|
+
*
|
|
499
|
+
* @element pds-locale
|
|
500
|
+
* @formAssociated
|
|
501
|
+
* @attr {string} [name] Form field name used during submit.
|
|
502
|
+
* @attr {string} [value] Selected canonical locale tag (`xx-YY`).
|
|
503
|
+
* @attr {boolean} [required] Requires a locale value for validity.
|
|
504
|
+
* @attr {boolean} [disabled] Disables interaction and omits form value.
|
|
505
|
+
* @attr {"compact"|"full"} [mode="compact"] Label display mode.
|
|
506
|
+
* In `compact`, the UI shows 2-letter aliases and puts the full Intl language
|
|
507
|
+
* name in the `title` attribute. In `full`, it renders the full Intl language
|
|
508
|
+
* name as the visible label.
|
|
509
|
+
* @attr {string} [data-label] Accessible label for the locale radio group.
|
|
510
|
+
* @prop {string} name Form field name.
|
|
511
|
+
* @prop {string} value Selected canonical locale tag (`xx-YY`).
|
|
512
|
+
* @prop {boolean} required Whether a value is required.
|
|
513
|
+
* @prop {boolean} disabled Whether interaction is disabled.
|
|
514
|
+
* @prop {"compact"|"full"} mode Label display mode.
|
|
515
|
+
* @prop {HTMLFormElement|null} form Associated form element.
|
|
516
|
+
* @prop {NodeListOf<HTMLLabelElement>|null} labels Associated labels.
|
|
517
|
+
* @prop {string} type Form control type.
|
|
518
|
+
* @prop {ValidityState|null} validity Current validity state.
|
|
519
|
+
* @prop {string} validationMessage Current validation message.
|
|
520
|
+
* @prop {boolean} willValidate Whether the control participates in validation.
|
|
521
|
+
* @fires pds-locale:ready Emitted after locale availability is resolved.
|
|
522
|
+
* @fires pds:locale:changed Emitted after a new locale is selected.
|
|
523
|
+
* @fires input Native-like input event when user selection changes.
|
|
524
|
+
* @fires change Native-like change event when user selection changes.
|
|
525
|
+
*/
|
|
526
|
+
class PdsLocale extends HTMLElement {
|
|
527
|
+
|
|
528
|
+
static formAssociated = true;
|
|
529
|
+
|
|
530
|
+
static get observedAttributes() {
|
|
531
|
+
return ["name", "value", "required", "disabled", "mode", "data-label"];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#observer;
|
|
535
|
+
#isReady = false;
|
|
536
|
+
#resolveReady;
|
|
537
|
+
#readyPromise;
|
|
538
|
+
#available = false;
|
|
539
|
+
#locales = [];
|
|
540
|
+
#radioName = `pds-locale-${Math.random().toString(36).slice(2, 10)}`;
|
|
541
|
+
#internals;
|
|
542
|
+
#defaultValue = "";
|
|
543
|
+
#capturedDefault = false;
|
|
544
|
+
#syncingValueAttribute = false;
|
|
545
|
+
#listening = false;
|
|
546
|
+
|
|
547
|
+
constructor() {
|
|
548
|
+
super();
|
|
549
|
+
this.attachShadow({ mode: "open" });
|
|
550
|
+
this.#internals = this.attachInternals?.() ?? null;
|
|
551
|
+
this.#readyPromise = new Promise((resolve) => {
|
|
552
|
+
this.#resolveReady = resolve;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
connectedCallback() {
|
|
557
|
+
if (!this.#capturedDefault) {
|
|
558
|
+
this.#defaultValue = this.getAttribute("value") || "";
|
|
559
|
+
this.#capturedDefault = true;
|
|
560
|
+
}
|
|
561
|
+
void this.#setup();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
disconnectedCallback() {
|
|
565
|
+
this.#teardownObserver();
|
|
566
|
+
|
|
567
|
+
if (this.#listening) {
|
|
568
|
+
this.shadowRoot.removeEventListener("change", this.#handleChange);
|
|
569
|
+
this.#listening = false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
574
|
+
if (oldValue === newValue) return;
|
|
575
|
+
|
|
576
|
+
if (name === "value" && this.#syncingValueAttribute) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (name === "data-label") {
|
|
581
|
+
const fieldset = this.shadowRoot.querySelector("fieldset[role='radiogroup']");
|
|
582
|
+
if (fieldset) {
|
|
583
|
+
fieldset.setAttribute("aria-label", this.getAttribute("data-label") || msg("Language"));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (name === "mode" && this.#available) {
|
|
588
|
+
this.#renderOptions(this.#locales);
|
|
589
|
+
this.#applyDisabledState();
|
|
590
|
+
this.#syncCheckedState();
|
|
591
|
+
this.#syncFormState();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (name === "value" && this.#available) {
|
|
596
|
+
this.#syncValueFromAttribute({ updateDocumentLang: true, emitEvents: false });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (name === "disabled") {
|
|
601
|
+
this.#applyDisabledState();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.#syncFormState();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
formAssociatedCallback() {}
|
|
608
|
+
|
|
609
|
+
formDisabledCallback(disabled) {
|
|
610
|
+
this.disabled = Boolean(disabled);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
formResetCallback() {
|
|
614
|
+
const candidateLocales = this.#locales.length
|
|
615
|
+
? this.#locales
|
|
616
|
+
: collectKnownFiveLetterLocales();
|
|
617
|
+
|
|
618
|
+
const resetLocale =
|
|
619
|
+
resolveFiveLetterLocaleTag(this.#defaultValue, candidateLocales) ||
|
|
620
|
+
candidateLocales[0] ||
|
|
621
|
+
"";
|
|
622
|
+
|
|
623
|
+
if (!resetLocale) {
|
|
624
|
+
this.#setValueAttribute("");
|
|
625
|
+
this.#syncCheckedState();
|
|
626
|
+
this.#syncFormState();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.#commitLocaleSelection(resetLocale, {
|
|
631
|
+
emitEvents: false,
|
|
632
|
+
updateDocumentLang: true,
|
|
633
|
+
reflectAttribute: true,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
formStateRestoreCallback(state) {
|
|
638
|
+
const candidateLocales = this.#locales.length
|
|
639
|
+
? this.#locales
|
|
640
|
+
: collectKnownFiveLetterLocales();
|
|
641
|
+
|
|
642
|
+
const restoredValue = typeof state === "string" ? state : "";
|
|
643
|
+
const restoredLocale = resolveFiveLetterLocaleTag(restoredValue, candidateLocales);
|
|
644
|
+
|
|
645
|
+
if (!restoredLocale) {
|
|
646
|
+
this.#syncValueFromAttribute({ updateDocumentLang: true, emitEvents: false });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.#commitLocaleSelection(restoredLocale, {
|
|
651
|
+
emitEvents: false,
|
|
652
|
+
updateDocumentLang: true,
|
|
653
|
+
reflectAttribute: true,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
checkValidity() {
|
|
658
|
+
this.#syncFormState();
|
|
659
|
+
return this.#internals?.checkValidity() ?? true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
reportValidity() {
|
|
663
|
+
this.#syncFormState();
|
|
664
|
+
return this.#internals?.reportValidity() ?? true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
get form() {
|
|
668
|
+
return this.#internals?.form ?? null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
get labels() {
|
|
672
|
+
return this.#internals?.labels ?? null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
get type() {
|
|
676
|
+
return "pds-locale";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
get validity() {
|
|
680
|
+
return this.#internals?.validity ?? null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
get validationMessage() {
|
|
684
|
+
return this.#internals?.validationMessage ?? "";
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
get willValidate() {
|
|
688
|
+
return this.#internals?.willValidate ?? false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
get name() {
|
|
692
|
+
return this.getAttribute("name") || "";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
set name(value) {
|
|
696
|
+
if (value == null || value === "") {
|
|
697
|
+
this.removeAttribute("name");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
this.setAttribute("name", String(value));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
get value() {
|
|
705
|
+
const candidateLocales = this.#locales.length
|
|
706
|
+
? this.#locales
|
|
707
|
+
: collectKnownFiveLetterLocales();
|
|
708
|
+
|
|
709
|
+
return resolveFiveLetterLocaleTag(this.getAttribute("value"), candidateLocales) || "";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
set value(value) {
|
|
713
|
+
const candidateLocales = this.#locales.length
|
|
714
|
+
? this.#locales
|
|
715
|
+
: collectKnownFiveLetterLocales();
|
|
716
|
+
|
|
717
|
+
const canonicalValue = resolveFiveLetterLocaleTag(value, candidateLocales);
|
|
718
|
+
if (!canonicalValue) {
|
|
719
|
+
if (this.#available && this.#locales.length) {
|
|
720
|
+
this.#syncValueFromAttribute({ updateDocumentLang: false, emitEvents: false });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
this.#setValueAttribute("");
|
|
725
|
+
this.#syncCheckedState();
|
|
726
|
+
this.#syncFormState();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.#setValueAttribute(canonicalValue);
|
|
731
|
+
|
|
732
|
+
if (this.#available) {
|
|
733
|
+
this.#commitLocaleSelection(canonicalValue, {
|
|
734
|
+
emitEvents: false,
|
|
735
|
+
updateDocumentLang: true,
|
|
736
|
+
reflectAttribute: false,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
get required() {
|
|
742
|
+
return this.hasAttribute("required");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
set required(value) {
|
|
746
|
+
this.toggleAttribute("required", Boolean(value));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
get disabled() {
|
|
750
|
+
return this.hasAttribute("disabled");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
set disabled(value) {
|
|
754
|
+
this.toggleAttribute("disabled", Boolean(value));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
get mode() {
|
|
758
|
+
return this.#resolveMode(this.getAttribute("mode"));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
set mode(value) {
|
|
762
|
+
if (value == null || value === "") {
|
|
763
|
+
this.removeAttribute("mode");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
this.setAttribute("mode", this.#resolveMode(value));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Resolves once the first availability check is complete.
|
|
772
|
+
*
|
|
773
|
+
* @returns {Promise<boolean>}
|
|
774
|
+
*/
|
|
775
|
+
whenReady() {
|
|
776
|
+
return this.#readyPromise;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Whether locale switching is currently available.
|
|
781
|
+
*
|
|
782
|
+
* @returns {boolean}
|
|
783
|
+
*/
|
|
784
|
+
get available() {
|
|
785
|
+
return this.#available;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Re-detect available locales and re-render.
|
|
790
|
+
*
|
|
791
|
+
* @returns {Promise<boolean>}
|
|
792
|
+
*/
|
|
793
|
+
async refresh() {
|
|
794
|
+
await this.#setup({ forceReloadLocales: true });
|
|
795
|
+
return this.#available;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async #setup({ forceReloadLocales = false } = {}) {
|
|
799
|
+
const componentStyles = PDS.createStylesheet(`
|
|
800
|
+
:host {
|
|
801
|
+
display: block;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
label span {
|
|
805
|
+
display: inline-flex;
|
|
806
|
+
gap: var(--spacing-xs, 0.35rem);
|
|
807
|
+
align-items: center;
|
|
808
|
+
}
|
|
809
|
+
`);
|
|
810
|
+
|
|
811
|
+
await PDS.adoptLayers(this.shadowRoot, LAYERS, [componentStyles]);
|
|
812
|
+
|
|
813
|
+
const localizationEnabled = isLocalizationActive();
|
|
814
|
+
const detectedLocales = localizationEnabled
|
|
815
|
+
? await getStartupLocalizationLocales({ forceReload: forceReloadLocales })
|
|
816
|
+
: [];
|
|
817
|
+
|
|
818
|
+
const knownFiveLetterLocales = collectKnownFiveLetterLocales();
|
|
819
|
+
|
|
820
|
+
const canonicalLocales = Array.from(
|
|
821
|
+
new Set(
|
|
822
|
+
detectedLocales
|
|
823
|
+
.map((locale) => resolveFiveLetterLocaleTag(locale, knownFiveLetterLocales))
|
|
824
|
+
.filter(Boolean)
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
if (!localizationEnabled || canonicalLocales.length < 2) {
|
|
829
|
+
this.#locales = [];
|
|
830
|
+
this.#available = false;
|
|
831
|
+
this.hidden = true;
|
|
832
|
+
this.shadowRoot.innerHTML = "";
|
|
833
|
+
this.#teardownObserver();
|
|
834
|
+
this.#syncFormState();
|
|
835
|
+
this.#emitReady(false);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
this.hidden = false;
|
|
840
|
+
this.#available = true;
|
|
841
|
+
this.#locales = [...canonicalLocales];
|
|
842
|
+
|
|
843
|
+
if (this.#defaultValue) {
|
|
844
|
+
this.#defaultValue = resolveFiveLetterLocaleTag(this.#defaultValue, this.#locales) || "";
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
this.#renderOptions(this.#locales);
|
|
848
|
+
|
|
849
|
+
if (!this.#listening) {
|
|
850
|
+
this.shadowRoot.addEventListener("change", this.#handleChange);
|
|
851
|
+
this.#listening = true;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
this.#attachObserver();
|
|
855
|
+
this.#syncValueFromAttribute({ updateDocumentLang: true, emitEvents: false });
|
|
856
|
+
this.#applyDisabledState();
|
|
857
|
+
this.#syncFormState();
|
|
858
|
+
this.#emitReady(true);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#syncValueFromAttribute({ updateDocumentLang = true, emitEvents = false } = {}) {
|
|
862
|
+
const localeFromValue = resolveFiveLetterLocaleTag(this.getAttribute("value"), this.#locales);
|
|
863
|
+
const localeFromDocument = resolveFiveLetterLocaleTag(
|
|
864
|
+
document.documentElement?.getAttribute?.("lang"),
|
|
865
|
+
this.#locales
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const nextLocale = localeFromValue || localeFromDocument || this.#locales[0] || "";
|
|
869
|
+
if (!nextLocale) {
|
|
870
|
+
this.#setValueAttribute("");
|
|
871
|
+
this.#syncCheckedState();
|
|
872
|
+
this.#syncFormState();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (!this.#defaultValue) {
|
|
877
|
+
this.#defaultValue = nextLocale;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
this.#commitLocaleSelection(nextLocale, {
|
|
881
|
+
emitEvents,
|
|
882
|
+
updateDocumentLang,
|
|
883
|
+
reflectAttribute: true,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
#renderOptions(locales) {
|
|
888
|
+
this.shadowRoot.innerHTML = "";
|
|
889
|
+
const renderMode = this.mode;
|
|
890
|
+
|
|
891
|
+
const form = document.createElement("form");
|
|
892
|
+
form.setAttribute("part", "form");
|
|
893
|
+
|
|
894
|
+
const fieldset = document.createElement("fieldset");
|
|
895
|
+
fieldset.setAttribute("part", "fieldset");
|
|
896
|
+
fieldset.className = "buttons";
|
|
897
|
+
fieldset.setAttribute("role", "radiogroup");
|
|
898
|
+
fieldset.setAttribute("aria-label", this.getAttribute("data-label") || msg("Language"));
|
|
899
|
+
|
|
900
|
+
locales.forEach((locale) => {
|
|
901
|
+
const canonicalLocale = resolveFiveLetterLocaleTag(locale, this.#locales);
|
|
902
|
+
if (!canonicalLocale) return;
|
|
903
|
+
const optionLabel = document.createElement("label");
|
|
904
|
+
optionLabel.setAttribute("part", "option");
|
|
905
|
+
|
|
906
|
+
const optionInput = document.createElement("input");
|
|
907
|
+
optionInput.type = "radio";
|
|
908
|
+
optionInput.name = this.#radioName;
|
|
909
|
+
optionInput.value = canonicalLocale;
|
|
910
|
+
|
|
911
|
+
const optionText = document.createElement("span");
|
|
912
|
+
const fullLocaleLabel = localeOptionLabel(canonicalLocale);
|
|
913
|
+
const localeAlias =
|
|
914
|
+
localeOptionAlias(canonicalLocale) || fullLocaleLabel || canonicalLocale;
|
|
915
|
+
optionText.textContent =
|
|
916
|
+
renderMode === "full"
|
|
917
|
+
? fullLocaleLabel || canonicalLocale
|
|
918
|
+
: localeAlias.toUpperCase();
|
|
919
|
+
|
|
920
|
+
if (fullLocaleLabel) {
|
|
921
|
+
if (renderMode === "compact") {
|
|
922
|
+
optionLabel.title = fullLocaleLabel;
|
|
923
|
+
optionText.title = fullLocaleLabel;
|
|
924
|
+
optionInput.setAttribute("aria-label", `${fullLocaleLabel} (${localeAlias})`);
|
|
925
|
+
} else {
|
|
926
|
+
optionInput.setAttribute("aria-label", fullLocaleLabel);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
optionLabel.append(optionInput, optionText);
|
|
931
|
+
fieldset.appendChild(optionLabel);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
form.appendChild(fieldset);
|
|
935
|
+
this.shadowRoot.appendChild(form);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
#handleChange = (event) => {
|
|
939
|
+
const selected = event.target;
|
|
940
|
+
if (!(selected instanceof HTMLInputElement) || selected.type !== "radio") {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const canonicalLocale = resolveFiveLetterLocaleTag(selected.value, this.#locales);
|
|
945
|
+
if (!canonicalLocale) return;
|
|
946
|
+
|
|
947
|
+
this.#commitLocaleSelection(canonicalLocale, {
|
|
948
|
+
emitEvents: true,
|
|
949
|
+
updateDocumentLang: true,
|
|
950
|
+
reflectAttribute: true,
|
|
951
|
+
});
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
#commitLocaleSelection(
|
|
955
|
+
locale,
|
|
956
|
+
{ emitEvents = false, updateDocumentLang = true, reflectAttribute = true } = {}
|
|
957
|
+
) {
|
|
958
|
+
const canonicalLocale = resolveFiveLetterLocaleTag(locale, this.#locales);
|
|
959
|
+
if (!canonicalLocale) {
|
|
960
|
+
this.#syncFormState();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const previousValue = this.value;
|
|
965
|
+
|
|
966
|
+
if (reflectAttribute) {
|
|
967
|
+
this.#setValueAttribute(canonicalLocale);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (updateDocumentLang) {
|
|
971
|
+
document.documentElement.setAttribute("lang", canonicalLocale);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
this.#syncCheckedState();
|
|
975
|
+
this.#syncFormState();
|
|
976
|
+
|
|
977
|
+
if (!emitEvents || previousValue === canonicalLocale) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
982
|
+
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
983
|
+
|
|
984
|
+
/** @type {CustomEvent<PdsLocaleChangedDetail>} */
|
|
985
|
+
const localeChangedEvent = new CustomEvent("pds:locale:changed", {
|
|
986
|
+
detail: { locale: canonicalLocale },
|
|
987
|
+
bubbles: true,
|
|
988
|
+
composed: true,
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
this.dispatchEvent(localeChangedEvent);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
#attachObserver() {
|
|
995
|
+
if (this.#observer || typeof document === "undefined") return;
|
|
996
|
+
|
|
997
|
+
this.#observer = new MutationObserver(() => {
|
|
998
|
+
const activeDocumentLocale = resolveFiveLetterLocaleTag(
|
|
999
|
+
document.documentElement?.getAttribute?.("lang"),
|
|
1000
|
+
this.#locales
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
if (!activeDocumentLocale) {
|
|
1004
|
+
this.#syncCheckedState();
|
|
1005
|
+
this.#syncFormState();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
this.#commitLocaleSelection(activeDocumentLocale, {
|
|
1010
|
+
emitEvents: false,
|
|
1011
|
+
updateDocumentLang: false,
|
|
1012
|
+
reflectAttribute: true,
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
this.#observer.observe(document.documentElement, {
|
|
1017
|
+
attributes: true,
|
|
1018
|
+
attributeFilter: ["lang"],
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
#teardownObserver() {
|
|
1023
|
+
if (!this.#observer) return;
|
|
1024
|
+
this.#observer.disconnect();
|
|
1025
|
+
this.#observer = undefined;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
#syncCheckedState() {
|
|
1029
|
+
if (!this.#locales.length) return;
|
|
1030
|
+
|
|
1031
|
+
const activeLocale =
|
|
1032
|
+
this.value ||
|
|
1033
|
+
normalizeLocaleTag(document.documentElement?.getAttribute?.("lang")) ||
|
|
1034
|
+
normalizeLocaleTag(PDS?.getLocalizationState?.()?.locale) ||
|
|
1035
|
+
this.#locales[0];
|
|
1036
|
+
|
|
1037
|
+
this.shadowRoot.querySelectorAll('input[type="radio"]').forEach((radio) => {
|
|
1038
|
+
radio.checked = localeMatches(radio.value, activeLocale);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
#applyDisabledState() {
|
|
1043
|
+
const isDisabled = this.disabled;
|
|
1044
|
+
this.toggleAttribute("aria-disabled", isDisabled);
|
|
1045
|
+
|
|
1046
|
+
this.shadowRoot.querySelectorAll('input[type="radio"]').forEach((radio) => {
|
|
1047
|
+
radio.disabled = isDisabled;
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#resolveMode(value) {
|
|
1052
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
1053
|
+
return normalized === "full" ? "full" : "compact";
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
#setValueAttribute(value) {
|
|
1057
|
+
const nextValue = value ? String(value) : "";
|
|
1058
|
+
|
|
1059
|
+
if (nextValue) {
|
|
1060
|
+
if (this.getAttribute("value") === nextValue) return;
|
|
1061
|
+
this.#syncingValueAttribute = true;
|
|
1062
|
+
this.setAttribute("value", nextValue);
|
|
1063
|
+
this.#syncingValueAttribute = false;
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!this.hasAttribute("value")) return;
|
|
1068
|
+
|
|
1069
|
+
this.#syncingValueAttribute = true;
|
|
1070
|
+
this.removeAttribute("value");
|
|
1071
|
+
this.#syncingValueAttribute = false;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
#syncFormState() {
|
|
1075
|
+
if (!this.#internals) return;
|
|
1076
|
+
|
|
1077
|
+
if (!this.#available || this.disabled) {
|
|
1078
|
+
this.#internals.setFormValue(null);
|
|
1079
|
+
this.#internals.setValidity({});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const checkedRadioValue = resolveFiveLetterLocaleTag(
|
|
1084
|
+
this.shadowRoot.querySelector('input[type="radio"]:checked')?.value,
|
|
1085
|
+
this.#locales
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
const formValue = this.value || checkedRadioValue || "";
|
|
1089
|
+
|
|
1090
|
+
if (!this.value && formValue) {
|
|
1091
|
+
this.#setValueAttribute(formValue);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
this.#internals.setFormValue(formValue || "");
|
|
1095
|
+
|
|
1096
|
+
if (this.required && !formValue) {
|
|
1097
|
+
const focusTarget =
|
|
1098
|
+
this.shadowRoot.querySelector('input[type="radio"]:checked') ||
|
|
1099
|
+
this.shadowRoot.querySelector('input[type="radio"]') ||
|
|
1100
|
+
this;
|
|
1101
|
+
|
|
1102
|
+
this.#internals.setValidity(
|
|
1103
|
+
{ valueMissing: true },
|
|
1104
|
+
msg("Please select a language."),
|
|
1105
|
+
focusTarget
|
|
1106
|
+
);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
this.#internals.setValidity({});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
#emitReady(available) {
|
|
1114
|
+
const detail = { available: Boolean(available), locales: [...this.#locales] };
|
|
1115
|
+
this.dispatchEvent(
|
|
1116
|
+
new CustomEvent("pds-locale:ready", {
|
|
1117
|
+
detail,
|
|
1118
|
+
bubbles: true,
|
|
1119
|
+
composed: true,
|
|
1120
|
+
})
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
if (!this.#isReady) {
|
|
1124
|
+
this.#isReady = true;
|
|
1125
|
+
this.#resolveReady(Boolean(available));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (!customElements.get("pds-locale")) {
|
|
1131
|
+
customElements.define("pds-locale", PdsLocale);
|
|
1132
|
+
}
|