@urbicon-ui/i18n 6.1.4
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/README.md +322 -0
- package/dist/components/I18nProvider.svelte +62 -0
- package/dist/components/I18nProvider.svelte.d.ts +28 -0
- package/dist/components/T.svelte +31 -0
- package/dist/components/T.svelte.d.ts +11 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/i18n/__fixtures__/SetLocaleChild.svelte +20 -0
- package/dist/i18n/__fixtures__/SetLocaleChild.svelte.d.ts +18 -0
- package/dist/i18n/__fixtures__/SetLocaleHarness.svelte +14 -0
- package/dist/i18n/__fixtures__/SetLocaleHarness.svelte.d.ts +6 -0
- package/dist/i18n/__fixtures__/SsrChild.svelte +7 -0
- package/dist/i18n/__fixtures__/SsrChild.svelte.d.ts +18 -0
- package/dist/i18n/__fixtures__/SsrHarness.svelte +15 -0
- package/dist/i18n/__fixtures__/SsrHarness.svelte.d.ts +7 -0
- package/dist/i18n/context.svelte.d.ts +107 -0
- package/dist/i18n/context.svelte.js +167 -0
- package/dist/i18n/package-integration.d.ts +118 -0
- package/dist/i18n/package-integration.js +211 -0
- package/dist/i18n/registry.svelte.d.ts +93 -0
- package/dist/i18n/registry.svelte.js +460 -0
- package/dist/i18n/resolve-locale.d.ts +41 -0
- package/dist/i18n/resolve-locale.js +81 -0
- package/dist/i18n/types.d.ts +127 -0
- package/dist/i18n/types.js +18 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/utils/deep-keys.d.ts +27 -0
- package/dist/utils/deep-keys.js +54 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# @urbicon-ui/i18n
|
|
2
|
+
|
|
3
|
+
Svelte 5 runes-based internationalization. SSR-correct, package-scoped, type-safe, zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Why a custom i18n package
|
|
6
|
+
|
|
7
|
+
Urbicon UI is zero-dependency by design, and existing i18n libraries either predate Svelte 5 runes (`svelte-i18n`), ship a large generic runtime (`i18next`), or compile per-app and so can't ship as a reusable component-library locale source (Paraglide). This package provides exactly what the design system needs: reactive translations via `$state`/`$derived`, a **request-scoped** locale (correct under SSR), and a registry each Urbicon package (blocks, table, auth) plugs into.
|
|
8
|
+
|
|
9
|
+
The locale lives in **context**, not a module-global singleton — so concurrent SSR requests with different locales never leak into each other. Static translation data stays module-global (it's request-identical); only the mutable active locale is per-request.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
This package ships inside the Urbicon UI monorepo. Install from repo root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Peer dependencies: `svelte` (^5.40 — uses runes + `createContext`-era context), `@sveltejs/kit`.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
**1. Mount one provider at your app root** and feed it the initial locale.
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<!-- +layout.svelte -->
|
|
27
|
+
<script>
|
|
28
|
+
import { I18nProvider } from '@urbicon-ui/i18n';
|
|
29
|
+
let { data, children } = $props();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<I18nProvider locale={data.locale}>
|
|
33
|
+
{@render children()}
|
|
34
|
+
</I18nProvider>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// +layout.server.ts — resolve the locale per request (SSR), cookie + Accept-Language
|
|
39
|
+
import { resolveLocale } from '@urbicon-ui/i18n';
|
|
40
|
+
export const load = ({ request }) => ({ locale: resolveLocale(request) });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Read translations in components through a hook** — `useI18n()` for the global surface, or a package's `use<Package>I18n()` for its typed keys.
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<script>
|
|
47
|
+
import { useI18n } from '@urbicon-ui/i18n';
|
|
48
|
+
const i18n = useI18n();
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<p>{i18n.t('greeting', { name: 'Ada' })}</p><p>{i18n.formatNumber(1234.5)}</p>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Without a provider, reads resolve against the **base locale** (`en`) — a `<Button>` renders its ARIA strings out of the box, SSR-safe, no setup. Switching the locale (below) requires a provider.
|
|
55
|
+
|
|
56
|
+
## Read-tolerant, write-strict
|
|
57
|
+
|
|
58
|
+
- **Reading** without a provider → the constant base locale (`en`). Zero-config, SSR-safe, identical on server and client (no hydration mismatch).
|
|
59
|
+
- **Writing** (`setLocale`) without a provider → **throws**. There is no request-scoped state to mutate — you forgot the provider. Loud by design.
|
|
60
|
+
|
|
61
|
+
## Locale switching
|
|
62
|
+
|
|
63
|
+
`setLocale` mutates the request-scoped state and re-renders reactively in place (no reload). The built-in `<LocaleSwitcher>` (from `@urbicon-ui/blocks`) does this for you; programmatically:
|
|
64
|
+
|
|
65
|
+
```svelte
|
|
66
|
+
<script>
|
|
67
|
+
import { useI18n } from '@urbicon-ui/i18n';
|
|
68
|
+
const i18n = useI18n();
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<button onclick={() => i18n.setLocale('de')}>Deutsch</button>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Persist the choice so the next SSR request renders it — the provider's `onLocaleChange` is the hook (write the cookie `resolveLocale` reads):
|
|
75
|
+
|
|
76
|
+
```svelte
|
|
77
|
+
<I18nProvider
|
|
78
|
+
locale={data.locale}
|
|
79
|
+
onLocaleChange={(l) =>
|
|
80
|
+
(document.cookie = `urbicon-locale=${l}; path=/; max-age=31536000; samesite=lax`)}
|
|
81
|
+
>
|
|
82
|
+
{@render children()}
|
|
83
|
+
</I18nProvider>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Root layout that itself renders translated chrome
|
|
87
|
+
|
|
88
|
+
A child `<I18nProvider>` can't serve the parent that mounts it (context only flows downward). When the **same** root component both provides i18n and renders translated chrome (header/footer), call `provideI18n` in its own script instead:
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<script>
|
|
92
|
+
import { provideI18n, useI18n } from '@urbicon-ui/i18n';
|
|
93
|
+
let { data, children } = $props();
|
|
94
|
+
provideI18n(() => data.locale); // controlled by the load function
|
|
95
|
+
const i18n = useI18n();
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<header>{i18n.t('chrome.appTitle')}</header>
|
|
99
|
+
{@render children()}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Package-scoped translations
|
|
103
|
+
|
|
104
|
+
Each Urbicon package registers its own namespaced keys so consumers get a merged, consistent translation surface without collisions. The factory returns a **hook**, `useTranslate`, re-exported as `use<Package>I18n`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// Inside @urbicon-ui/blocks — src/lib/i18n/index.ts
|
|
108
|
+
import { createPackageI18n } from '@urbicon-ui/i18n';
|
|
109
|
+
import en from '../translations/en';
|
|
110
|
+
import de from '../translations/de';
|
|
111
|
+
|
|
112
|
+
export const blocksI18n = createPackageI18n('blocks', { en, de });
|
|
113
|
+
|
|
114
|
+
// The context-scoped hook (re-exported for components)
|
|
115
|
+
export const useBlocksI18n = blocksI18n.useTranslate;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```svelte
|
|
119
|
+
<!-- In a blocks component -->
|
|
120
|
+
<script>
|
|
121
|
+
import { useBlocksI18n } from '$lib';
|
|
122
|
+
const bt = useBlocksI18n(); // call during component init
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<button aria-label={bt('dialog.close')}>×</button>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`bt('dialog.close')` reads the context locale **at call time**, so wrapping it in markup / `$derived` re-renders on locale change. Resolution falls back to the package's base locale, then the global namespace.
|
|
129
|
+
|
|
130
|
+
## Type-safe keys
|
|
131
|
+
|
|
132
|
+
`createPackageI18n` is generic over the `en` bundle: with `as const` (or a plain literal object) the key **and** parameter types flow straight through to the hook's `t`, so keys autocomplete and typos are compile errors.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const en = {
|
|
136
|
+
dialog: { close: 'Close' },
|
|
137
|
+
greeting: 'Hello {{name}}'
|
|
138
|
+
} as const;
|
|
139
|
+
|
|
140
|
+
const blocks = createPackageI18n('blocks', { en /*, de */ });
|
|
141
|
+
const t = blocks.useTranslate(); // inside a component
|
|
142
|
+
|
|
143
|
+
t('dialog.close'); // ✓ autocompletes
|
|
144
|
+
t('dialog.nonexistent'); // ✗ compile error — unknown key
|
|
145
|
+
t('greeting', { name: 'Ada' }); // ✓ param `name` inferred from {{name}}
|
|
146
|
+
t('greeting'); // ✗ compile error — missing required param
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Additional **eager** locales are checked against the `en` structure, so a missing or misspelled key in `de` is a compile error too (key parity by construction). For **lazy** locales (below) parity is a runtime check — pair with `validatePackageTranslations` in a test.
|
|
150
|
+
|
|
151
|
+
> `createTypedPackage` is **deprecated** — `createPackageI18n` gives the same type safety directly.
|
|
152
|
+
|
|
153
|
+
## Pluralization
|
|
154
|
+
|
|
155
|
+
`useI18n().plural` selects the CLDR category via `Intl.PluralRules` (correct for any BCP-47 locale). Provide a `<key>_plural` entry as a JSON object of categories:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// translations
|
|
159
|
+
{ apple: '{{count}} apple', apple_plural: '{"one":"{{count}} apple","other":"{{count}} apples"}' }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```svelte
|
|
163
|
+
<script>
|
|
164
|
+
const i18n = useI18n();
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<span>{i18n.plural('apple', { count: 3 })}</span> <!-- 3 apples -->
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Plural rules follow [Unicode CLDR](https://cldr.unicode.org/index/cldr-spec/plural-rules); `en`/`de` collapse to `one`/`other`, Slavic locales add `few`/`many`, Arabic uses the full set. Without a `_plural` object the base string is returned as-is (fail-honest — no anglocentric `+'s'` guessing).
|
|
171
|
+
|
|
172
|
+
## SSR — resolving the initial locale
|
|
173
|
+
|
|
174
|
+
`resolveLocale` derives the request's locale server-side from the persisted cookie, then `Accept-Language`, then a default. Framework-agnostic (`Request` or a `{ cookie, acceptLanguage }` object):
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { resolveLocale } from '@urbicon-ui/i18n';
|
|
178
|
+
|
|
179
|
+
resolveLocale(request); // -> 'de'
|
|
180
|
+
resolveLocale(request, {
|
|
181
|
+
supportedLocales: ['en', 'de'],
|
|
182
|
+
defaultLocale: 'en',
|
|
183
|
+
cookieName: 'urbicon-locale'
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`supportedLocales` defaults to the locales the registry actually has data for. Feed the result to `<I18nProvider locale={…}>` so SSR and the first client render agree (no hydration mismatch, no `navigator.language` guess).
|
|
188
|
+
|
|
189
|
+
> Fully **prerendered** (static) sites have no per-request server, so resolve the locale on the client after mount instead (read a cookie/`localStorage`, then `setLocale`). The provider's base-locale-first render keeps hydration stable.
|
|
190
|
+
|
|
191
|
+
## Locale code-splitting (opt-in)
|
|
192
|
+
|
|
193
|
+
By default a package registers all its locale bundles eagerly. To keep non-base locales out of the initial bundle, register them as dynamic-import loaders — the base/fallback locale stays eager, the rest load on activation:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
export const blocksI18n = createPackageI18n(
|
|
197
|
+
'blocks',
|
|
198
|
+
{ en }, // eager base
|
|
199
|
+
{
|
|
200
|
+
loaders: {
|
|
201
|
+
de: () => import('../translations/de').then((m) => m.default),
|
|
202
|
+
fr: () => import('../translations/fr').then((m) => m.default)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Vite/Rollup splits each dynamic import into its own chunk, so only the active locale is in the initial bundle. The provider loads the active + fallback locale on mount; `setLocale` loads a target on switch. A lazy non-base initial locale renders the fallback until its chunk lands, then re-resolves reactively. Worth it past a handful of locales; eager is simpler for `en`/`de`.
|
|
209
|
+
|
|
210
|
+
## Coexisting with an app-level i18n (e.g. Paraglide)
|
|
211
|
+
|
|
212
|
+
If your app uses Paraglide (or any other i18n) for its **own** strings, you don't run two locale states — you make Urbicon's provider follow the app's locale. Pass the app-i18n locale into the provider as a controlled (reactive) value:
|
|
213
|
+
|
|
214
|
+
```svelte
|
|
215
|
+
<!-- +layout.svelte -->
|
|
216
|
+
<script>
|
|
217
|
+
import { I18nProvider } from '@urbicon-ui/i18n';
|
|
218
|
+
import { getLocale } from '$lib/paraglide/runtime'; // Paraglide's reactive locale
|
|
219
|
+
let { children } = $props();
|
|
220
|
+
</script>
|
|
221
|
+
|
|
222
|
+
<!-- getLocale() is reactive → the provider re-syncs when the app switches language -->
|
|
223
|
+
<I18nProvider locale={getLocale()}>
|
|
224
|
+
{@render children()}
|
|
225
|
+
</I18nProvider>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
When the app switches language (Paraglide's `setLocale`), `getLocale()` updates, the provider's controlled-sync pushes it into Urbicon's state, and every Urbicon component re-renders in the new language — one switch, both layers. (If you also expose an Urbicon `<LocaleSwitcher>`, route its `onLocaleChange` back into the app's `setLocale` so the two never diverge.)
|
|
229
|
+
|
|
230
|
+
> Map locale codes if they differ between systems (e.g. Paraglide `en-US` → Urbicon `en`) before passing them in.
|
|
231
|
+
|
|
232
|
+
## Error handling
|
|
233
|
+
|
|
234
|
+
Loader failures and unsupported-locale switches default to `console.warn`. Route them to telemetry by configuring an app-global handler **once at startup** (it lives on the process-wide registry — do not set it per request):
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { configureI18n } from '@urbicon-ui/i18n';
|
|
238
|
+
configureI18n({ onError: (e) => reportToSentry(e) });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Parity validation (CI)
|
|
242
|
+
|
|
243
|
+
`validatePackageTranslations` does a recursive deep-key diff across a package's locale bundles (a missing nested key is an error, an extra one a warning). Wire it into a per-package vitest test to fail CI on drift — it complements the compile-time parity the generic factory enforces for eager bundles, and covers lazy/dynamic ones:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { validatePackageTranslations } from '@urbicon-ui/i18n';
|
|
247
|
+
import { blocksTranslations } from '$lib/i18n';
|
|
248
|
+
|
|
249
|
+
it('en/de key parity', () => {
|
|
250
|
+
expect(validatePackageTranslations('blocks', blocksTranslations).errors).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## API Surface
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// Provider + hooks + server helper
|
|
258
|
+
import {
|
|
259
|
+
I18nProvider, // <I18nProvider locale fallbackLocale? onLocaleChange?>
|
|
260
|
+
provideI18n, // provide from a component's own script (root layouts)
|
|
261
|
+
useI18n, // { locale, setLocale, availableLocales, isLoading, t, plural, exists, formatNumber, ... }
|
|
262
|
+
configureI18n, // app-global error sink
|
|
263
|
+
resolveLocale, // server-side initial-locale resolution
|
|
264
|
+
T, // <T key params? fallback? package? />
|
|
265
|
+
BASE_LOCALE, // 'en'
|
|
266
|
+
SUPPORTED_LOCALES,
|
|
267
|
+
isLocaleSupported
|
|
268
|
+
} from '@urbicon-ui/i18n';
|
|
269
|
+
|
|
270
|
+
// Package integration
|
|
271
|
+
import {
|
|
272
|
+
createPackageI18n, // (name, { en }, { loaders }?) -> { useTranslate, t, exists, getLocales, ... }
|
|
273
|
+
createComponentI18n,
|
|
274
|
+
registerTranslationLoaders,
|
|
275
|
+
registerPackages,
|
|
276
|
+
validatePackageTranslations
|
|
277
|
+
} from '@urbicon-ui/i18n';
|
|
278
|
+
|
|
279
|
+
// Deep-key utilities + types
|
|
280
|
+
import { getDeepValue, hasDeepKey, collectDeepKeys } from '@urbicon-ui/i18n';
|
|
281
|
+
import type {
|
|
282
|
+
Locale,
|
|
283
|
+
I18nApi,
|
|
284
|
+
PackageI18n,
|
|
285
|
+
CreatePackageI18nOptions,
|
|
286
|
+
I18nConfigureOptions,
|
|
287
|
+
LocaleSource,
|
|
288
|
+
ResolveLocaleOptions,
|
|
289
|
+
TranslationParams,
|
|
290
|
+
TranslationOptions,
|
|
291
|
+
PluralParams,
|
|
292
|
+
PluralRules,
|
|
293
|
+
TypedTranslationFunction,
|
|
294
|
+
DeepKeys,
|
|
295
|
+
DeepValue
|
|
296
|
+
} from '@urbicon-ui/i18n';
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
> **Breaking (major):** the pre-WP2 module singleton (`i18n`, the free `t` / `plural`, `I18nService`) was **removed** — it leaked the locale across SSR requests. Mount `<I18nProvider>` / `provideI18n` and read through the hooks; replace `i18n.t(k)` → `useI18n().t(k)`, `bt(k)` → `const bt = useBlocksI18n()`.
|
|
300
|
+
|
|
301
|
+
## Supported Locales (Core)
|
|
302
|
+
|
|
303
|
+
`en`, `de` ship data. `fr`, `es`, `it`, `nl` are declared target locales (in the `Locale` union / `SUPPORTED_LOCALES`) — register your own bundles for them via `createPackageI18n`.
|
|
304
|
+
|
|
305
|
+
## Development
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
bun --filter='@urbicon-ui/i18n' run dev # svelte-package watch
|
|
309
|
+
bun --filter='@urbicon-ui/i18n' run build # svelte-package
|
|
310
|
+
bun --filter='@urbicon-ui/i18n' run test:run # vitest
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Related
|
|
314
|
+
|
|
315
|
+
- [`@urbicon-ui/blocks`](../blocks/) — consumes this package; exports `useBlocksI18n`, `<LocaleSwitcher>`
|
|
316
|
+
- [`@urbicon-ui/table`](../table/) — ships its own namespace (`table.*`), exports `useTableI18n`
|
|
317
|
+
- [`@urbicon-ui/auth`](../auth/) — ships EN/DE bundles; exports `useAuthLocale`
|
|
318
|
+
- [Architecture Overview](../../docs/ARCHITECTURE.md#i18n-system)
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import { provideI18n } from '../i18n/context.svelte';
|
|
4
|
+
import type { Locale } from '../i18n/types';
|
|
5
|
+
|
|
6
|
+
interface I18nProviderProps {
|
|
7
|
+
/**
|
|
8
|
+
* Active locale. The single request-scoped i18n value — provide it from
|
|
9
|
+
* server-resolved state (cookie/Accept-Language via `resolveLocale`) so SSR
|
|
10
|
+
* and hydration agree. May be a reactive (controlled) value; prop changes are
|
|
11
|
+
* synced into the internal state.
|
|
12
|
+
* @default 'en'
|
|
13
|
+
*/
|
|
14
|
+
locale?: Locale;
|
|
15
|
+
/**
|
|
16
|
+
* Locale used when a key is missing in the active locale.
|
|
17
|
+
* @default 'en'
|
|
18
|
+
*/
|
|
19
|
+
fallbackLocale?: Locale;
|
|
20
|
+
/**
|
|
21
|
+
* Fired when the *effective* locale changes — both via `setLocale()`
|
|
22
|
+
* (LocaleSwitcher, programmatic) and via a `locale`-prop change. The place to
|
|
23
|
+
* persist the choice (e.g. write the locale cookie that `resolveLocale` reads
|
|
24
|
+
* on the next request).
|
|
25
|
+
*/
|
|
26
|
+
onLocaleChange?: (locale: Locale) => void;
|
|
27
|
+
children: Snippet;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
locale = 'en',
|
|
32
|
+
fallbackLocale = 'en',
|
|
33
|
+
onLocaleChange,
|
|
34
|
+
children
|
|
35
|
+
}: I18nProviderProps = $props();
|
|
36
|
+
|
|
37
|
+
// One reactive state object per provider instance → per request tree on the
|
|
38
|
+
// server. This is what makes concurrent SSR requests with different locales
|
|
39
|
+
// render independently: no module-global mutable locale is involved. provideI18n
|
|
40
|
+
// creates + provides the state and keeps it in controlled sync with the
|
|
41
|
+
// `locale` prop (an in-place setLocale switch is never clobbered).
|
|
42
|
+
const state = provideI18n(
|
|
43
|
+
() => locale,
|
|
44
|
+
untrack(() => fallbackLocale)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Notify on effective-locale change (from either source), skipping the initial
|
|
48
|
+
// value. Tracks `state.locale`; the bookkeeping is untracked so it adds no
|
|
49
|
+
// dependencies and never loops.
|
|
50
|
+
let lastNotified = untrack(() => locale);
|
|
51
|
+
$effect(() => {
|
|
52
|
+
const current = state.locale;
|
|
53
|
+
untrack(() => {
|
|
54
|
+
if (current !== lastNotified) {
|
|
55
|
+
lastNotified = current;
|
|
56
|
+
onLocaleChange?.(current);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
{@render children()}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { Locale } from '../i18n/types';
|
|
3
|
+
interface I18nProviderProps {
|
|
4
|
+
/**
|
|
5
|
+
* Active locale. The single request-scoped i18n value — provide it from
|
|
6
|
+
* server-resolved state (cookie/Accept-Language via `resolveLocale`) so SSR
|
|
7
|
+
* and hydration agree. May be a reactive (controlled) value; prop changes are
|
|
8
|
+
* synced into the internal state.
|
|
9
|
+
* @default 'en'
|
|
10
|
+
*/
|
|
11
|
+
locale?: Locale;
|
|
12
|
+
/**
|
|
13
|
+
* Locale used when a key is missing in the active locale.
|
|
14
|
+
* @default 'en'
|
|
15
|
+
*/
|
|
16
|
+
fallbackLocale?: Locale;
|
|
17
|
+
/**
|
|
18
|
+
* Fired when the *effective* locale changes — both via `setLocale()`
|
|
19
|
+
* (LocaleSwitcher, programmatic) and via a `locale`-prop change. The place to
|
|
20
|
+
* persist the choice (e.g. write the locale cookie that `resolveLocale` reads
|
|
21
|
+
* on the next request).
|
|
22
|
+
*/
|
|
23
|
+
onLocaleChange?: (locale: Locale) => void;
|
|
24
|
+
children: Snippet;
|
|
25
|
+
}
|
|
26
|
+
declare const I18nProvider: import("svelte").Component<I18nProviderProps, {}, "">;
|
|
27
|
+
type I18nProvider = ReturnType<typeof I18nProvider>;
|
|
28
|
+
export default I18nProvider;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '../i18n/context.svelte';
|
|
3
|
+
import type { TranslationParams, TranslationOptions } from '../i18n/types';
|
|
4
|
+
|
|
5
|
+
interface TProps {
|
|
6
|
+
key: string;
|
|
7
|
+
params?: TranslationParams;
|
|
8
|
+
fallback?: string;
|
|
9
|
+
package?: string;
|
|
10
|
+
options?: TranslationOptions;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { key, params, fallback, package: packageName, options }: TProps = $props();
|
|
14
|
+
|
|
15
|
+
// Resolves against the request-scoped locale from the nearest <I18nProvider>,
|
|
16
|
+
// or the base locale when none is mounted (read-tolerant). Captured at init.
|
|
17
|
+
const i18n = useI18n();
|
|
18
|
+
|
|
19
|
+
// Reactive translation that updates when locale changes
|
|
20
|
+
const translation = $derived.by(() => {
|
|
21
|
+
const opts: TranslationOptions = {
|
|
22
|
+
...options,
|
|
23
|
+
packageName: packageName || options?.packageName
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const result = i18n.t(key, params, opts);
|
|
27
|
+
return result === key && fallback ? fallback : result;
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{translation}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TranslationParams, TranslationOptions } from '../i18n/types';
|
|
2
|
+
interface TProps {
|
|
3
|
+
key: string;
|
|
4
|
+
params?: TranslationParams;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
package?: string;
|
|
7
|
+
options?: TranslationOptions;
|
|
8
|
+
}
|
|
9
|
+
declare const T: import("svelte").Component<TProps, {}, "">;
|
|
10
|
+
type T = ReturnType<typeof T>;
|
|
11
|
+
export default T;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '../context.svelte';
|
|
3
|
+
|
|
4
|
+
// Attempt an in-place switch during init and render the outcome. Referencing
|
|
5
|
+
// `outcome` in the markup forces the instance script to run under SSR; the IIFE
|
|
6
|
+
// keeps it a single const assignment (no reactive-update warning).
|
|
7
|
+
const i18n = useI18n();
|
|
8
|
+
const outcome = (() => {
|
|
9
|
+
try {
|
|
10
|
+
i18n.setLocale('de');
|
|
11
|
+
return 'no-error';
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return err instanceof Error && /I18nProvider/.test(err.message)
|
|
14
|
+
? 'needs-provider'
|
|
15
|
+
: 'other-error';
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<span>{outcome}</span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const SetLocaleChild: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type SetLocaleChild = InstanceType<typeof SetLocaleChild>;
|
|
18
|
+
export default SetLocaleChild;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import I18nProvider from '../../components/I18nProvider.svelte';
|
|
3
|
+
import SetLocaleChild from './SetLocaleChild.svelte';
|
|
4
|
+
|
|
5
|
+
let { provide }: { provide?: boolean } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#if provide}
|
|
9
|
+
<I18nProvider locale="en">
|
|
10
|
+
<SetLocaleChild />
|
|
11
|
+
</I18nProvider>
|
|
12
|
+
{:else}
|
|
13
|
+
<SetLocaleChild />
|
|
14
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const SsrChild: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type SsrChild = InstanceType<typeof SsrChild>;
|
|
18
|
+
export default SsrChild;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import I18nProvider from '../../components/I18nProvider.svelte';
|
|
3
|
+
import SsrChild from './SsrChild.svelte';
|
|
4
|
+
import type { Locale } from '../types';
|
|
5
|
+
|
|
6
|
+
let { locale }: { locale?: Locale } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
{#if locale}
|
|
10
|
+
<I18nProvider {locale}>
|
|
11
|
+
<SsrChild />
|
|
12
|
+
</I18nProvider>
|
|
13
|
+
{:else}
|
|
14
|
+
<SsrChild />
|
|
15
|
+
{/if}
|