@lilydesignsystem/svelte-locale-picker 0.1.0

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/index.md ADDED
@@ -0,0 +1,571 @@
1
+ # LocalePicker (Svelte helper)
2
+
3
+ A reusable, headless Svelte 5 locale picker — an **icon button that
4
+ opens a WAI-ARIA APG listbox** — that applies the chosen locale to the
5
+ document root via `lang` and `dir`, with optional `localStorage`
6
+ persistence and `navigator.languages` detection.
7
+
8
+ For the full contract see [spec/index.md](./spec/index.md) — it is the single source
9
+ of truth for the API, behaviour, and tests.
10
+
11
+ ## Install
12
+
13
+ This directory is published as a folder-style import; consumers either
14
+ copy it into their project or wire it as a workspace dependency. The
15
+ only runtime dependency is `svelte` ≥ 5.
16
+
17
+ ```ts
18
+ import LocalePicker from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
19
+ ```
20
+
21
+ Or via the barrel (recommended; gives you the typed helpers too):
22
+
23
+ ```ts
24
+ import LocalePicker, {
25
+ bcp47LocaleTag,
26
+ isRtlLocale,
27
+ localeName,
28
+ type Props,
29
+ type ChildArgs,
30
+ } from "./lily-design-system-svelte-locale-picker";
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ Render the select with a `label` and the list of locales your app
36
+ supports. The select writes `lang` and `dir` onto `<html>` so your
37
+ i18n library, your CSS (`html[dir="rtl"]`), and assistive technology
38
+ all see the change.
39
+
40
+ ```svelte
41
+ <script lang="ts">
42
+ import LocalePicker, {
43
+ bcp47LocaleTag,
44
+ localeName,
45
+ } from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
46
+
47
+ let locale = $state("");
48
+ </script>
49
+
50
+ <LocalePicker
51
+ label="Language"
52
+ locales={["en", "en_US", "fr", "fr_CA", "ar", "he"]}
53
+ bind:value={locale}
54
+ storageKey="lily-locale"
55
+ detectFromNavigator
56
+ />
57
+
58
+ <p class="locale-picker-status" aria-live="polite">
59
+ Active language:
60
+ <span lang={bcp47LocaleTag(locale)}>{localeName(locale)}</span>
61
+ </p>
62
+ ```
63
+
64
+ You also need to **style the listbox** — the package ships zero CSS,
65
+ and an unpositioned popup renders in normal flow and shoves the page
66
+ down when it opens. Use logical properties, since this control flips
67
+ the page to RTL. See
68
+ [docs/styling.md § Positioning the listbox](./docs/styling.md#positioning-the-listbox).
69
+
70
+ The status line is recommended, though no longer strictly compensatory.
71
+ The listbox marks the active option with `aria-selected="true"`, so a
72
+ screen-reader user who opens the control does hear which locale is
73
+ current. But the _closed_ control shows only a glyph — and unlike a
74
+ theme select, the active locale is not something a user can infer by
75
+ looking, unless they can already read the page, which is the one thing
76
+ this control cannot assume. `aria-live="polite"` announces mutations
77
+ only, so it stays silent on first paint and speaks once per change, and
78
+ the `lang` on the `<span>` keeps the locale name pronounced in its own
79
+ language. Full reasoning and when to omit it:
80
+ [docs/accessibility.md](./docs/accessibility.md#the-status-region).
81
+
82
+ > The example above wraps the name in `lang` while showing
83
+ > `localeName`, which returns the **English** name — so drop the `lang`
84
+ > unless you are supplying endonyms via `localeLabels`. See the note in
85
+ > [docs/accessibility.md](./docs/accessibility.md#the-status-region).
86
+
87
+ When the user picks `ar`, the component:
88
+
89
+ - sets `lang="ar"` on `<html>`,
90
+ - sets `dir="rtl"` on `<html>` (auto-detected from the locale),
91
+ - writes `"ar"` to `localStorage["lily-locale"]`,
92
+ - fires `onChange("ar")` if provided.
93
+
94
+ The select does NOT translate strings — that is the consumer's
95
+ i18n library (e.g. `svelte-i18n`, Paraglide, Inlang, Tolgee, raw
96
+ `Intl.*`). Wire the bindable `value` or `onChange` to your library so
97
+ it loads the right messages.
98
+
99
+ ## BCP 47 normalisation
100
+
101
+ Language tags follow **BCP 47** (RFC 5646). The `lang` attribute on
102
+ HTML elements must use hyphens, while many applications carry locale
103
+ identifiers with underscores (`en_US`, `zh_Hant_TW`). The select
104
+ accepts whichever form you prefer in the `locales` array and converts
105
+ to the hyphen form when writing to the DOM. The bindable `value`
106
+ preserves your original form, so round-trips are lossless.
107
+
108
+ ```ts
109
+ bcp47LocaleTag("en_US"); // "en-US"
110
+ bcp47LocaleTag("zh_Hant_TW"); // "zh-Hant-TW"
111
+ bcp47LocaleTag("en"); // "en"
112
+ ```
113
+
114
+ References:
115
+
116
+ - W3C — [Language tags in HTML and XML](https://www.w3.org/International/articles/language-tags/)
117
+ - IETF — [RFC 5646 (BCP 47), Tags for Identifying Languages](https://www.rfc-editor.org/rfc/rfc5646)
118
+ - IANA — [Language Subtag Registry (registry file)](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)
119
+
120
+ ## RTL auto-detection
121
+
122
+ `isRtlLocale(locale)` returns `true` for any locale whose base
123
+ language is one of `ar`, `arc`, `ckb`, `dv`, `fa`, `he`, `iw`, `ji`,
124
+ `ks`, `ku`, `mzn`, `ps`, `sd`, `ug`, `ur`, `yi`, OR whose script
125
+ subtag is one of `Arab`, `Hebr`, `Thaa`, `Syrc`, `Nkoo`, `Mong`,
126
+ `Adlm`.
127
+
128
+ ```ts
129
+ isRtlLocale("ar"); // true
130
+ isRtlLocale("he_IL"); // true
131
+ isRtlLocale("uz_Arab_AF"); // true (script subtag)
132
+ isRtlLocale("en"); // false
133
+ ```
134
+
135
+ Pass `applyDir={false}` if you want full control of `dir` yourself.
136
+
137
+ ## Examples
138
+
139
+ ### Rendered markup
140
+
141
+ ```svelte
142
+ <script lang="ts">
143
+ import LocalePicker from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
144
+ let locale = $state("en");
145
+ </script>
146
+
147
+ <LocalePicker label="Language" locales={["en", "cy"]} bind:value={locale} />
148
+ ```
149
+
150
+ Renders:
151
+
152
+ ```html
153
+ <div class="locale-picker">
154
+ <input type="hidden" name="locale" value="en" />
155
+ <button
156
+ type="button"
157
+ class="locale-picker-button"
158
+ aria-label="Language"
159
+ aria-haspopup="listbox"
160
+ aria-expanded="false"
161
+ aria-controls="locale-picker-1-list"
162
+ >
163
+ <svg class="locale-picker-icon" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M2 8h12"/><path d="M8 2c2.2 0 4 2.7 4 6s-1.8 6-4 6-4-2.7-4-6 1.8-6 4-6z"/></svg>
164
+ </button>
165
+ <ul
166
+ class="locale-picker-list"
167
+ id="locale-picker-1-list"
168
+ role="listbox"
169
+ aria-label="Language"
170
+ tabindex="-1"
171
+ hidden
172
+ >
173
+ <li
174
+ class="locale-picker-option"
175
+ id="locale-picker-1-option-0"
176
+ role="option"
177
+ aria-selected="true"
178
+ lang="en"
179
+ >
180
+ English
181
+ </li>
182
+ <li
183
+ class="locale-picker-option"
184
+ id="locale-picker-1-option-1"
185
+ role="option"
186
+ aria-selected="false"
187
+ lang="cy"
188
+ >
189
+ Welsh
190
+ </li>
191
+ </ul>
192
+ </div>
193
+ ```
194
+
195
+ Each locale option carries its own `lang` attribute so a screen reader
196
+ pronounces "Cymraeg" with a Welsh voice (WCAG 3.1.2, Language of
197
+ Parts). The button and the list carry none — they are in whatever
198
+ language you wrote `label` in.
199
+
200
+ The glyph is U+1F310 GLOBE WITH MERIDIANS followed by **U+FE0E
201
+ VARIATION SELECTOR-15**, which forces monochrome text presentation so
202
+ the control matches `theme-picker`'s `◑` instead of rendering as a blue
203
+ colour emoji. It is `aria-hidden`; the accessible name comes from
204
+ `label`.
205
+
206
+ The hidden input keeps the control working inside a `<form>`, carrying
207
+ the consumer-form code.
208
+
209
+ ### Why an icon button
210
+
211
+ The closed control costs one glyph of page width whether you offer
212
+ three locales or all 436 in `locales.tsv`. A native `<select>` is as
213
+ wide as its longest option, or truncates it.
214
+
215
+ This shape has three real costs — an icon-only control's name rests
216
+ entirely on `aria-label` (which is itself written in _one_ language,
217
+ for the one control a user reaches when they cannot read the page); a
218
+ hand-rolled listbox has weaker assistive-technology support than a
219
+ native `<select>`; and the glyph is a font-dependent character that may
220
+ substitute, render in colour, or fail to render. They are set out in
221
+ full, with mitigations, in
222
+ [docs/accessibility.md](./docs/accessibility.md). **For some audiences
223
+ a native `<select>` is the better choice** — read that page before
224
+ adopting this helper in an accessibility-critical or public-service
225
+ context.
226
+
227
+ ### Keyboard
228
+
229
+ Follows the WAI-ARIA APG
230
+ [Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
231
+ Every key is implemented by the component — none of it comes from the
232
+ platform.
233
+
234
+ On the **button**:
235
+
236
+ | Key | Action |
237
+ | -------------------------------- | ------------------------------------- |
238
+ | `Enter` / `Space` / `Arrow Down` | Open with the selected option active. |
239
+ | `Arrow Up` | Open with the **last** option active. |
240
+
241
+ On the **listbox** (focus moves there on open):
242
+
243
+ | Key | Action |
244
+ | ------------------------- | -------------------------------------------------- |
245
+ | `Arrow Down` / `Arrow Up` | Move the active option; **clamps**, does not wrap. |
246
+ | `Home` / `End` | Jump to the first / last option. |
247
+ | `Enter` / `Space` | Select, apply, close, refocus the button. |
248
+ | `Escape` | Close and refocus **without** changing the locale. |
249
+ | `PageUp` / `PageDown` | Move the active option by ten; clamps. |
250
+ | `Tab` | Close; focus lands on the button so the default Tab proceeds from the picker's position. |
251
+ | Printable character | Typeahead over the labels; 500 ms buffer. A single character advances to the next match and repeats cycle; differing characters refine. |
252
+
253
+ Clicking an option selects it; clicking outside or moving focus out of
254
+ the root closes the listbox.
255
+
256
+ Typeahead matches the **label**, and default labels are endonyms, so
257
+ a user types "Fra" for "Français". With `localeLabels` overrides the
258
+ consumer's spelling wins. Choose deliberately for long lists.
259
+
260
+ ### Pretty labels for the option text
261
+
262
+ By default each option shows the language's **endonym** — its own name
263
+ for itself, "Cymraeg" not "Welsh" — via `Intl.DisplayNames` asked in
264
+ that language (exported as `localeEndonym`). The user who needs a
265
+ language menu is the one who cannot read the page's language, and the
266
+ exonym means nothing to them. The English names from `locales.tsv` and
267
+ the raw code remain as fallbacks for runtimes without the data.
268
+ Override per-code with `localeLabels`:
269
+
270
+ ```svelte
271
+ <LocalePicker
272
+ label="Langue"
273
+ locales={["en", "fr", "ar"]}
274
+ localeLabels={{ en: "English", fr: "Français", ar: "العربية" }}
275
+ bind:value={locale}
276
+ />
277
+ ```
278
+
279
+ Each option carries a `lang="…"` attribute so each one is
280
+ announced in its own language. Prefer endonyms — the person who needs
281
+ this control is the person who cannot read your default language.
282
+
283
+ ### Customising the button
284
+
285
+ The `children` snippet **replaces the glyph inside the trigger
286
+ button**. It receives `{ value, open, labelFor }` and does **not**
287
+ render the options — the listbox is component-owned.
288
+
289
+ Pairing the glyph with the active locale's endonym is the strongest
290
+ mitigation for the icon-only naming tradeoff:
291
+
292
+ ```svelte
293
+ <script lang="ts">
294
+ import LocalePicker, {
295
+ bcp47LocaleTag,
296
+ isRtlLocale,
297
+ } from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
298
+
299
+ let locale = $state("en");
300
+ </script>
301
+
302
+ <LocalePicker
303
+ label="Language"
304
+ locales={["en", "fr", "ar"]}
305
+ localeLabels={{ en: "English", fr: "Français", ar: "العربية" }}
306
+ bind:value={locale}
307
+ >
308
+ {#snippet children({ value, open, labelFor })}
309
+ <svg viewBox="0 0 16 16" aria-hidden="true">…</svg>
310
+ <span
311
+ class="locale-picker-text"
312
+ lang={bcp47LocaleTag(value)}
313
+ dir={isRtlLocale(value) ? "rtl" : "ltr"}
314
+ >
315
+ {labelFor(value)}
316
+ </span>
317
+ <span aria-hidden="true">{open ? "▴" : "▾"}</span>
318
+ {/snippet}
319
+ </LocalePicker>
320
+ ```
321
+
322
+ The `lang` on the span is only correct because the labels are endonyms —
323
+ which the built-in labels now are. The component applies the same rule
324
+ to its own options: `lang` is set only when the label is the derived
325
+ endonym, never on consumer labels of unknown language.
326
+
327
+ The snippet's output lives inside a `<button>`, so it must not contain
328
+ interactive elements. The pre-listbox patterns built on the old
329
+ `ChildArgs` — a custom `<select>`, a radio group, a button group, a
330
+ `<datalist>` combobox — are no longer possible; read `value` and drive
331
+ your own controls instead. See
332
+ [docs/custom-rendering.md](./docs/custom-rendering.md).
333
+
334
+ ### Wiring an i18n library
335
+
336
+ ```svelte
337
+ <script lang="ts">
338
+ import LocalePicker from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
339
+ import { locale as i18nLocale } from "svelte-i18n"; // or Paraglide, Inlang, …
340
+
341
+ let current = $state("");
342
+ </script>
343
+
344
+ <LocalePicker
345
+ label="Language"
346
+ locales={["en", "fr", "ar"]}
347
+ bind:value={current}
348
+ detectFromNavigator
349
+ storageKey="app-locale"
350
+ onChange={(code) => i18nLocale.set(code)}
351
+ />
352
+ ```
353
+
354
+ ### Server-resolved initial value (SSR)
355
+
356
+ For flicker-free first paint, resolve the locale on the server (from a
357
+ cookie or `Accept-Language`) and pass it as `value`:
358
+
359
+ ```svelte
360
+ <script lang="ts">
361
+ let { initialLocale }: { initialLocale: string } = $props();
362
+ let locale = $state(initialLocale);
363
+ </script>
364
+
365
+ <LocalePicker
366
+ label="Language"
367
+ locales={["en", "fr", "ar"]}
368
+ value={locale}
369
+ bind:value={locale}
370
+ />
371
+ ```
372
+
373
+ During SSR the component renders the button and the (hidden) listbox,
374
+ with `aria-selected` and the hidden input reflecting the supplied
375
+ `value`, and the document already arrives with the correct `lang`
376
+ attribute on `<html>` — which is what prevents the flicker. Option ids
377
+ come from an incrementing module counter, so server and client agree
378
+ and hydration matches.
379
+
380
+ ### Render into a scoped target instead of `<html>`
381
+
382
+ Set `target` to a specific element when you want the locale scoped to a
383
+ region (e.g. a multilingual side panel):
384
+
385
+ ```svelte
386
+ <script lang="ts">
387
+ import LocalePicker from "./lily-design-system-svelte-locale-picker/LocalePicker.svelte";
388
+ let region: HTMLElement | null = $state(null);
389
+ let panelLocale = $state("fr");
390
+ </script>
391
+
392
+ <section bind:this={region}>
393
+ <p>This panel switches language independently of the page.</p>
394
+ <LocalePicker
395
+ label="Panel language"
396
+ locales={["en", "fr", "ar"]}
397
+ target={region}
398
+ bind:value={panelLocale}
399
+ />
400
+ </section>
401
+ ```
402
+
403
+ `<html>` stays in the page's default locale; the section gets the
404
+ chosen one.
405
+
406
+ ## Built-in locale data
407
+
408
+ `locales.ts` ships the 436 codes from `locales.tsv` mapped to their
409
+ English names. Since the endonym change these are a **fallback** — the
410
+ default label is `localeEndonym(code)`, and the table is consulted only
411
+ when the runtime lacks `Intl.DisplayNames` data for a code. You can also import
412
+ the data directly:
413
+
414
+ ```ts
415
+ import {
416
+ defaultLocaleLabels,
417
+ RTL_LANGUAGE_TAGS,
418
+ RTL_SCRIPT_SUBTAGS,
419
+ } from "./lily-design-system-svelte-locale-picker";
420
+
421
+ console.log(defaultLocaleLabels["en_US"]); // "English (United States)"
422
+ console.log(RTL_LANGUAGE_TAGS.has("ar")); // true
423
+ ```
424
+
425
+ ## Props
426
+
427
+ See [spec/index.md §4](./spec/index.md#4-public-api) for the full table.
428
+
429
+ Required props: `label`, `locales`.
430
+
431
+ Common optional props: `value` (bindable), `defaultValue`,
432
+ `storageKey`, `detectFromNavigator`, `localeLabels`, `applyDir`,
433
+ `target`, `onChange`, `class`, `name`, `children`.
434
+
435
+ **There is no `placeholder` prop.** It was removed along with the
436
+ native `<select>` it belonged to. Field-by-field reference:
437
+ [docs/props-reference.md](./docs/props-reference.md).
438
+
439
+ ## Class hooks
440
+
441
+ `.locale-picker` (root `<div>`), `.locale-picker-button` (the trigger),
442
+ `.locale-picker-icon` (the glyph span), `.locale-picker-list` (the
443
+ `<ul role="listbox">`), `.locale-picker-option` (each
444
+ `<li role="option">`). Plus `[data-active]` for the keyboard cursor and
445
+ `[aria-selected]` for the applied locale — style both.
446
+
447
+ The `.locale-picker-placeholder` hook is gone with the placeholder
448
+ option.
449
+
450
+ The package ships zero CSS, so **you must position the listbox**, using
451
+ logical properties so it survives the RTL switch this control performs.
452
+ See [docs/styling.md](./docs/styling.md).
453
+
454
+ ## Accessibility
455
+
456
+ - Built to the WAI-ARIA APG **Listbox** pattern: a `<button
457
+ aria-haspopup="listbox">` controlling a `<ul role="listbox">` whose
458
+ active option is tracked with `aria-activedescendant`.
459
+ - `aria-label={label}` names both the button and the listbox.
460
+ - The full keyboard contract is implemented by the component — see
461
+ [Keyboard](#keyboard).
462
+ - Each `<li role="option">` carries `lang="…"` so its name is
463
+ pronounced in the right language (WCAG 3.1.2, Language of Parts).
464
+ - The document root carries `lang` and (by default) `dir` so the page
465
+ satisfies WCAG 3.1.1 (Language of Page) and bidi text/layout
466
+ inverts correctly for RTL locales.
467
+ - The active state is exposed four ways: `aria-selected` on the option,
468
+ `lang` on the target, the hidden input's value, and the `value`
469
+ binding. No colour-only meaning.
470
+ - Choosing a locale returns focus to the trigger button and does not
471
+ navigate — WCAG 3.2.2 (On Input).
472
+
473
+ **Three tradeoffs, stated plainly:**
474
+
475
+ 1. The button is icon-only, so its accessible name rests **entirely**
476
+ on `aria-label` — and `aria-label` is written in _one_ language.
477
+ This is the control a user reaches for precisely when they cannot
478
+ read the page, so the circularity is real. Pairing the glyph with
479
+ the active locale's endonym via `children` is the strongest
480
+ mitigation.
481
+ 2. A hand-rolled listbox has **weaker assistive-technology support**
482
+ than a native `<select>` — particularly on mobile, where a native
483
+ select opens the OS picker. For some audiences, and public-service
484
+ audiences especially, a plain `<select aria-label>` with one
485
+ `<option lang>` per locale is genuinely the better choice; it is
486
+ about fifteen lines, and this package's exported pure helpers still
487
+ do the logic.
488
+ 3. The glyph is a **font-dependent character**. VS15 requests
489
+ monochrome presentation but cannot guarantee it, and on a device
490
+ with no covering font the button renders empty or as a "tofu" box.
491
+
492
+ Each has mitigations. Read
493
+ [docs/accessibility.md](./docs/accessibility.md) before adopting this
494
+ helper in an accessibility-critical context.
495
+
496
+ ## Tests
497
+
498
+ `pnpm test` under a vitest + jsdom + `@testing-library/svelte` setup
499
+ exercises every numbered acceptance clause in
500
+ [spec/index.md §7](./spec/index.md#7-testing-acceptance-criteria) — 27
501
+ clauses covering the markup contract, the pure helpers, locale
502
+ application, initial-value resolution, spread + custom children, and
503
+ the APG keyboard contract, plus four untagged extras for
504
+ case-insensitive RTL detection and the navigator matcher.
505
+
506
+ ## Files in this directory
507
+
508
+ | File | Purpose |
509
+ | ----------------------- | ----------------------------------------------------------------- |
510
+ | `spec/index.md` | Single source of truth — API, behaviour, tests. |
511
+ | `LocalePicker.svelte` | The component implementation. |
512
+ | `LocalePicker.test.ts` | vitest suite covering every spec §7 item. |
513
+ | `locales.ts` | Built-in code → English-name map and RTL sets. |
514
+ | `locales.tsv` | Canonical 436-row source for `locales.ts`. |
515
+ | `index.ts` | Re-export barrel. |
516
+ | `index.md` | This file — quick start + worked examples. |
517
+ | `docs/` | Deep-dive guides — see [Documentation](#documentation). |
518
+ | `examples/` | Runnable Svelte 5 example components — see [Examples](#examples). |
519
+
520
+ ## Documentation
521
+
522
+ Shared with `theme-picker` (same topics, written for this helper):
523
+
524
+ | Guide | Covers |
525
+ | ------------------------------------------------------ | ------------------------------------------------------------------- |
526
+ | [docs/props-reference.md](./docs/props-reference.md) | Field-by-field reference for every prop, with rationale. |
527
+ | [docs/styling.md](./docs/styling.md) | Class and attribute hooks, positioning the listbox, RTL-safe CSS. |
528
+ | [docs/custom-rendering.md](./docs/custom-rendering.md) | The `children` snippet — replacing the button's glyph. |
529
+ | [docs/recipes.md](./docs/recipes.md) | Short solutions to adjacent problems. |
530
+ | [docs/troubleshooting.md](./docs/troubleshooting.md) | Symptoms, root causes, fixes. |
531
+ | [docs/accessibility.md](./docs/accessibility.md) | APG listbox contract, the three tradeoffs, screen-reader matrix. |
532
+ | [docs/ssr.md](./docs/ssr.md) | Cookie, URL-prefix, Accept-Language, streaming SSR, FOUC avoidance. |
533
+
534
+ Specific to locale-picker (no `theme-picker` counterpart):
535
+
536
+ | Guide | Covers |
537
+ | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
538
+ | [docs/concepts.md](./docs/concepts.md) | Mental model, lifecycle diagram, why the defaults are what they are. |
539
+ | [docs/bcp47.md](./docs/bcp47.md) | Language-tag syntax (RFC 5646), IANA registry, subtag composition. |
540
+ | [docs/rtl.md](./docs/rtl.md) | What's auto-detected, what `dir="rtl"` actually changes, CSS tips. |
541
+ | [docs/i18n-integration.md](./docs/i18n-integration.md) | Wiring svelte-i18n, Paraglide, Tolgee, raw `Intl.*`, SvelteKit URL strategies. |
542
+
543
+ `theme-picker`'s `preloading.md` has no counterpart here — it is about
544
+ stylesheet preloading, which this helper does not do.
545
+
546
+ ## Examples
547
+
548
+ Each file in `examples/` is a complete, runnable Svelte 5 component
549
+ you can copy into your project.
550
+
551
+ | Example | Demonstrates |
552
+ | ------------------------------------------------------------- | --------------------------------------------------------------------- |
553
+ | [basic.svelte](./examples/basic.svelte) | The default rendering, plus the `.locale-picker-status` live region. |
554
+ | [custom-rendering.svelte](./examples/custom-rendering.svelte) | `children` snippet — globe + the active locale's endonym + caret. |
555
+ | [many-locales.svelte](./examples/many-locales.svelte) | A 23-locale list in a one-glyph control; typeahead and scrolling. |
556
+ | [persistence.svelte](./examples/persistence.svelte) | `storageKey` plus `detectFromNavigator` on first visit. |
557
+ | [rtl-demo.svelte](./examples/rtl-demo.svelte) | Live RTL preview — Arabic, Hebrew, Persian, Urdu, Pashto. |
558
+ | [nhs-style.svelte](./examples/nhs-style.svelte) | NHS UK-style utility banner with endonyms and a `class` hook. |
559
+ | [with-svelte-i18n.svelte](./examples/with-svelte-i18n.svelte) | Binding to svelte-i18n's `locale` store. |
560
+ | [with-paraglide.svelte](./examples/with-paraglide.svelte) | Driving Paraglide JS's `setLocale()` from `onChange`. |
561
+ | [ssr-cookie.svelte](./examples/ssr-cookie.svelte) | SvelteKit cookie-based SSR — no flash of default locale. |
562
+ | [scoped-target.svelte](./examples/scoped-target.svelte) | Multiple per-region selects, each scoped to its own panel. |
563
+
564
+ These were previously numbered `01-radios`, `02-select`, `03-buttons`,
565
+ … — names left over from a radio-group rendering the package has not
566
+ had for some time. The mapping is recorded in
567
+ [examples/README.md](./examples/README.md#renamed-from-the-radio-group-era).
568
+
569
+ ---
570
+
571
+ Lily™ and Lily Design System™ are trademarks.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@lilydesignsystem/svelte-locale-picker",
3
+ "version": "0.1.0",
4
+ "engines": {
5
+ "node": "=26"
6
+ },
7
+ "description": "Lily Design System Svelte 5 locale picker: an icon button opening an APG listbox that sets lang and dir on the document. Headless, SSR-safe, no CSS.",
8
+ "type": "module",
9
+ "svelte": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "svelte": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "index.md",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "prepublishOnly": "cd .. && npm run build"
25
+ },
26
+ "peerDependencies": {
27
+ "svelte": "^5.0.0"
28
+ },
29
+ "keywords": [
30
+ "lily",
31
+ "design",
32
+ "system",
33
+ "svelte",
34
+ "locale",
35
+ "picker"
36
+ ],
37
+ "author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
38
+ "license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/LilyDesignSystem/lily-design-system-svelte-helpers.git",
42
+ "directory": "lily-design-system-svelte-locale-picker"
43
+ },
44
+ "homepage": "https://lilydesignsystem.com/",
45
+ "bugs": {
46
+ "url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
47
+ }
48
+ }