@sveltekit-i18n/base 1.3.8 → 3.0.0-next.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/README.md CHANGED
@@ -19,13 +19,15 @@ Core i18n functionality for SvelteKit with support for custom message parsers. T
19
19
 
20
20
  ## Key Features
21
21
 
22
- ✅ **SvelteKit ready** – Full SSR and CSR support
22
+ ✅ **Svelte 5 runes** – One reactive instance, no stores
23
+ ✅ **Framework ready** – Full SSR and CSR support
23
24
  ✅ **Parser-agnostic** – Use any message syntax you need
24
25
  ✅ **Custom data sources** – Load translations from anywhere (files, APIs, databases)
25
26
  ✅ **Module-based** – Translations load only for visited pages
26
27
  ✅ **Route-aware** – Automatic loading based on SvelteKit routes
27
28
  ✅ **Component-scoped** – Multiple translation instances with custom definitions
28
- ✅ **TypeScript** – Full type support
29
+ ✅ **Extensible** – Pipe the instance through [extensions](#extensions) to reshape or augment its surface
30
+ ✅ **TypeScript** – Locales inferred from your config, keys and payloads from a [`schema`](#schema)
29
31
  ✅ **Zero dependencies** – Lightweight and fast
30
32
 
31
33
  ## Installation
@@ -47,7 +49,7 @@ npm install @sveltekit-i18n/parser-icu
47
49
 
48
50
  ### 1. Create translation files
49
51
 
50
- ```json
52
+ ```jsonc
51
53
  // src/lib/translations/en/common.json
52
54
  {
53
55
  "greeting": "Hello, {{name}}!",
@@ -59,10 +61,10 @@ npm install @sveltekit-i18n/parser-icu
59
61
 
60
62
  ```javascript
61
63
  // src/lib/translations/index.js
62
- import i18n from '@sveltekit-i18n/base';
64
+ import { I18n } from '@sveltekit-i18n/base';
63
65
  import parser from '@sveltekit-i18n/parser-default';
64
66
 
65
- /** @type {import('@sveltekit-i18n/base').Config} */
67
+ /** @type {import('@sveltekit-i18n/base').Config.T} */
66
68
  const config = {
67
69
  parser: parser({ /* parser options */ }),
68
70
  loaders: [
@@ -79,36 +81,50 @@ const config = {
79
81
  ],
80
82
  };
81
83
 
82
- export const { t, locale, locales, loading, loadTranslations } = new i18n(config);
84
+ // One reactive instance. Do NOT destructure its value properties reading
85
+ // them off the instance is what makes templates reactive. (`t`/`l` are
86
+ // functions and stay reactive even when destructured, since the tracked reads
87
+ // happen at call time. In a component, `const { loading } = $derived(i18n)`
88
+ // destructures value reads without losing reactivity.)
89
+ export const i18n = new I18n(config);
83
90
  ```
84
91
 
85
92
  ### 3. Load translations in your layout
86
93
 
87
94
  ```javascript
88
95
  // src/routes/+layout.js
89
- import { loadTranslations } from '$lib/translations';
96
+ import { i18n } from '$lib/translations';
90
97
 
91
98
  /** @type {import('./$types').LayoutLoad} */
92
99
  export const load = async ({ url }) => {
93
100
  const { pathname } = url;
94
101
  const initLocale = 'en';
95
-
96
- await loadTranslations(initLocale, pathname);
97
-
102
+
103
+ await i18n.loadTranslations(initLocale, pathname);
104
+
98
105
  return {};
99
106
  };
100
107
  ```
101
108
 
109
+ > **Rendering per-visitor locales on the server?** The instance above is a
110
+ > module-level singleton — on the server it is shared by every request in the
111
+ > process, so concurrent visitors overwrite each other's locale. Use one
112
+ > instance per request and hand its data to the client with `snapshot()`:
113
+ > see [Server-Side Rendering](./docs/README.md#server-side-rendering).
114
+
102
115
  ### 4. Use in components
103
116
 
104
117
  ```svelte
105
118
  <script>
106
- import { t } from '$lib/translations';
119
+ import { i18n } from '$lib/translations';
107
120
  </script>
108
121
 
109
- <p>{$t('common.greeting', { name: 'World' })}</p>
122
+ <p>{i18n.t('common.greeting', { name: 'World' })}</p>
110
123
  ```
111
124
 
125
+ The call reads the reactive translation table and locale, so the text updates
126
+ automatically when either changes — no stores, no `$` prefix.
127
+
112
128
  ## Using Different Parsers
113
129
 
114
130
  ### ICU Message Format
@@ -170,6 +186,8 @@ loaders: [
170
186
  ]
171
187
  ```
172
188
 
189
+ Both `loaders` and a loader's `routes` accept readonly arrays, so a whole-config `as const` is fine.
190
+
173
191
  ### `translations`
174
192
 
175
193
  Synchronous translations loaded immediately:
@@ -221,15 +239,61 @@ preprocess: 'full' // 'full' | 'preserveArrays' | 'none' | custom function
221
239
  - `'none'`: No preprocessing
222
240
  - Custom function: `(input) => transformedOutput`
223
241
 
242
+ ### `schema`
243
+
244
+ A map of translation key to the payload its message expects (`never` for a message that takes none). Supplying it types `t`/`l` — keys autocomplete, an unknown key is a type error, and the payload argument is checked. Only its type is read, so the value can stay empty at runtime:
245
+
246
+ ```typescript
247
+ type TranslationSchema = {
248
+ 'common.greeting': { name: string };
249
+ 'common.farewell': never;
250
+ };
251
+
252
+ const i18n = new I18n({ ...config, schema: {} as TranslationSchema });
253
+ ```
254
+
255
+ Hand-write it for a small set of messages, or point the slot at a generated artifact. A schema whose keys are not a closed set is ignored, and keys stay plain strings. See [`schema`](./docs/README.md#schema) for the full rules.
256
+
224
257
  ### `cache`
225
258
 
226
- Server-side cache duration in milliseconds:
259
+ Time in milliseconds the loaded translations stay fresh for. By default, loaded translations never expire — loaders run once per locale and key (a loader's `routes` only decide whether a load trigger considers it, not how often it runs).
260
+
261
+ Set a finite value when your loaders fetch from a source that can change at runtime (e.g. a CMS):
227
262
 
228
263
  ```javascript
229
- cache: 86400000 // Default: 24 hours
264
+ cache: 3600000 // Translations older than 1 hour refetch on the next load
230
265
  ```
231
266
 
232
- Set to `Number.POSITIVE_INFINITY` to disable cache refresh.
267
+ Set to `0` to treat translations as always stale (refetch on every load trigger). You can also drop the loaded state manually at any time with [`invalidate()`](#methods).
268
+
269
+ ### `extensions`
270
+
271
+ Pipes the constructed instance through extension functions, left to right. Each extension receives the surface produced so far (the raw instance for the first one) and returns the surface handed on — `new I18n(config)` evaluates to the last extension's output:
272
+
273
+ ```javascript
274
+ import stores from '@sveltekit-i18n/extension-stores';
275
+
276
+ const { t, locale, loading } = new I18n({
277
+ ...config,
278
+ extensions: [stores],
279
+ });
280
+ ```
281
+
282
+ An extension may augment the instance in place, or replace the surface entirely (like the store adapter above). Official extensions live in the [extensions](https://github.com/sveltekit-i18n/extensions) repository; a custom extension is just a function:
283
+
284
+ ```javascript
285
+ const withGreeting = (i18n) => Object.assign(i18n, {
286
+ greet: (name) => i18n.t('common.greeting', { name }),
287
+ });
288
+
289
+ export const i18n = new I18n({ ...config, extensions: [withGreeting] });
290
+
291
+ i18n.greet('World');
292
+ ```
293
+
294
+ **Notes:**
295
+ - Applied at construction time only — a later `loadConfig()` call ignores this property.
296
+ - When an extension returns a new object, the result is no longer `instanceof I18n`; the original instance stays reachable through whatever the extension exposes (the official extensions expose it as `instance`).
233
297
 
234
298
  ### `log`
235
299
 
@@ -245,20 +309,39 @@ log: {
245
309
 
246
310
  ## API Reference
247
311
 
248
- ### Stores
312
+ ### Reactive properties
249
313
 
250
- - `t` – Translation function store
251
- - `locale` – Current locale (writable)
252
- - `locales` – Available locales (readable)
253
- - `loading` – Loading state (readable)
254
- - `initialized` – Initialization state (readable)
255
- - `translations` – All loaded translations (readable)
314
+ - `t(key, ...params)` – translate for the active locale (reactive function)
315
+ - `l(locale, key, ...params)` – translate for an explicit locale
316
+ - `locale` – the ACTIVE locale; assignment is a fire-and-forget `setLocale()`
317
+ - `locales` – available locales
318
+ - `loading` – `true` while any load is in flight
319
+ - `initialized` – locale and route set, translations present
320
+ - `translations` / `rawTranslations` – the (pre/post-preprocess) tables
256
321
 
257
322
  ### Methods
258
323
 
259
- - `loadTranslations(locale, route)` Load translations for locale and route
260
- - `setLocale(locale)` – Change current locale
261
- - `setRoute(route)` – Update current route
324
+ Load-triggering methods return the promise of the matching load — concurrent duplicate triggers share one in-flight load (and its promise) instead of fetching twice.
325
+
326
+ - `loadTranslations(locale, route?)` – load translations for locale and route; `route` defaults to the current one
327
+ - `setLocale(locale)` – request a locale; loads once a route is known
328
+ - `setRoute(route)` – update the current route
329
+ - `loadConfig(config)` – (re)configure the instance
330
+ - `addTranslations(translations)` – add synchronous translations
331
+ - `snapshot()` – serialize the active locale (and the fallback) for the current route, shaped like `config.translations` so the receiving instance hydrates from it
332
+ - `invalidate(locale?)` – mark loaded translations stale (one locale, or all); loaders run again on the next load trigger, and a load still in flight for an invalidated locale settles with its data discarded
333
+ - `destroy()` – detach a per-request or per-component instance: in-flight loads settle discarded, further load and mutation calls are ignored, reads keep working
334
+
335
+ ### Utilities
336
+
337
+ Two helpers the instance uses internally ship from a separate subpath, for code that has to match the library's own behavior:
338
+
339
+ ```javascript
340
+ import { sanitizeLocales, toDotNotation } from '@sveltekit-i18n/base/utils';
341
+ ```
342
+
343
+ - `toDotNotation(input, preserveArrays?)` – the flattening behind [`preprocess`](#preprocess), for a custom `preprocess` that still wants dot notation
344
+ - `sanitizeLocales(...locales)` – normalizes a locale from a URL, cookie or `Accept-Language` header the way the instance does, so it can be compared against `locale`
262
345
 
263
346
  Full API documentation: [docs/README.md](./docs/README.md)
264
347
 
@@ -272,21 +355,36 @@ Full API documentation: [docs/README.md](./docs/README.md)
272
355
  ## TypeScript Support
273
356
 
274
357
  ```typescript
275
- import i18n, { type Config } from '@sveltekit-i18n/base';
358
+ import { I18n, type Config } from '@sveltekit-i18n/base';
276
359
  import parser from '@sveltekit-i18n/parser-default';
277
- import type { Config as ParserConfig } from '@sveltekit-i18n/parser-default';
278
360
 
279
- const config: Config<ParserConfig> = {
361
+ // The parser's params – the rest parameters of `t`/`l`. Annotate only when the
362
+ // config lives on its own; `new I18n({ ... })` infers them.
363
+ type Params = [payload?: Record<string, unknown>];
364
+
365
+ const config: Config.T<Params> = {
280
366
  parser: parser(),
281
367
  loaders: [/* ... */],
282
368
  };
283
369
  ```
284
370
 
371
+ Two more things are inferred from the config itself. [`schema`](#schema) types the keys and payloads of `t`/`l`, and every locale the config names — loader locales, `initLocale`, `fallbackLocale` and the keys of `translations` — completes the locale arguments and reads (`setLocale`, `loadTranslations`, `invalidate`, `l`, `locale`, `locales`):
372
+
373
+ ```typescript
374
+ const i18n = new I18n({ parser: parser(), initLocale: 'en', fallbackLocale: 'de' });
375
+
376
+ i18n.setLocale('en'); // 'en' | 'de' autocomplete here
377
+ i18n.setLocale('sv'); // still accepted — the union is a hint, not a constraint
378
+ ```
379
+
380
+ The locales survive only when the config reaches the constructor as a literal — inline, as above, or a separate object with `as const`. An annotated or separately widened config, and any config with one dynamic locale source (`loaders: locales.map(...)`), leaves them plain `string`. See [TypeScript](./docs/README.md#typescript) for both.
381
+
285
382
  ## Related Packages
286
383
 
287
384
  - [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) – Complete solution with default parser
288
385
  - [@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default) – Default message parser
289
386
  - [@sveltekit-i18n/parser-icu](https://github.com/sveltekit-i18n/parsers/tree/master/parser-icu) – ICU message format parser
387
+ - [Extensions](https://github.com/sveltekit-i18n/extensions) – Official extensions for the `config.extensions` pipe
290
388
 
291
389
  ## Contributing
292
390
 
@@ -0,0 +1,80 @@
1
+ import type { Config, Extension, Parser, Schema, Translations } from './types.js';
2
+ declare class I18nCore<ParserParams extends Parser.Params = any, ParserOutput = string, TranslationSchema = never, LocaleUnion extends string = string> {
3
+ #private;
4
+ constructor(config?: Config.T<ParserParams, ParserOutput>);
5
+ /**
6
+ * The active locale. Reading it is reactive; assigning it is a shorthand for
7
+ * a fire-and-forget `setLocale()` — the value therefore updates once the
8
+ * locale's translations resolved, not synchronously on assignment.
9
+ */
10
+ get locale(): Config.LocaleInput<LocaleUnion> | undefined;
11
+ set locale(value: Config.LocaleInput<LocaleUnion> | undefined);
12
+ get translations(): Translations.SerializedTranslations;
13
+ get rawTranslations(): Translations.SerializedTranslations;
14
+ loading: boolean;
15
+ locales: Config.LocaleInput<LocaleUnion>[];
16
+ initialized: boolean;
17
+ /**
18
+ * Translates `key` for the active locale. Reactive wherever reads are
19
+ * tracked: the call reads the translation table and locale, so a component
20
+ * using `{i18n.t('key')}` re-renders when either changes.
21
+ */
22
+ t: Translations.TranslationFunction<ParserParams, ParserOutput, TranslationSchema>;
23
+ /** Like `t`, for an explicit locale. */
24
+ l: Translations.LocalTranslationFunction<ParserParams, ParserOutput, TranslationSchema, LocaleUnion>;
25
+ /**
26
+ * Applies a config. Overridable extension seam — `sveltekit-i18n` wires its
27
+ * default parser by extending this method.
28
+ */
29
+ configLoader(config: Config.T<ParserParams, ParserOutput>): Promise<void>;
30
+ /**
31
+ * Public entry for (re)configuration. The failure is reported here and the
32
+ * promise marked handled, so a fire-and-forget call cannot become an
33
+ * unhandled rejection; an awaiting caller still receives it.
34
+ */
35
+ loadConfig: (config: Config.T<ParserParams, ParserOutput>) => Promise<void>;
36
+ setLocale: (locale?: Config.LocaleInput<LocaleUnion>) => Promise<void>;
37
+ setRoute: (route: string) => Promise<void>;
38
+ loadTranslations: (locale: Config.LocaleInput<LocaleUnion>, route?: string) => Promise<void>;
39
+ /**
40
+ * Marks loaded translations stale — for one locale, or all of them. Loaders
41
+ * run again on the NEXT load trigger; the call itself starts no load and
42
+ * keeps the currently displayed translations in place. A load still in
43
+ * flight for an invalidated locale is severed: it settles, but its data is
44
+ * discarded — it predates the invalidation.
45
+ */
46
+ invalidate: (locale?: Config.LocaleInput<LocaleUnion>) => void;
47
+ addTranslations: (translations?: Translations.SerializedTranslations) => void;
48
+ /**
49
+ * Serializes what this instance holds for the active locale and the fallback
50
+ * locale, narrowed to the current route: a key owned only by loaders that do
51
+ * not match the route is left out. The result is shaped like
52
+ * `config.translations`, so a client hydrates by handing it back to the
53
+ * constructor — the bookkeeping derived from it then keeps the matching
54
+ * loaders from fetching the same data again.
55
+ */
56
+ snapshot: () => Translations.SerializedTranslations;
57
+ /**
58
+ * Detaches the instance from its loading lifecycle: in-flight loads settle
59
+ * with their data discarded, `loading` drops to `false`, and every further
60
+ * load or mutation call is ignored with a warning. Reads (`t`, `l`, `locale`,
61
+ * `translations`, `snapshot`) keep working, so a component still tearing down
62
+ * renders its last state instead of breaking. Idempotent.
63
+ */
64
+ destroy: () => void;
65
+ }
66
+ /**
67
+ * A class declaration cannot annotate its constructor's return type, so the
68
+ * extension pipe's construction-time type lives on this construct signature
69
+ * instead: parser params and output are inferred from `config.parser`, locales
70
+ * are narrowed to the ones the config names, and the returned surface is the
71
+ * instance type folded through the `config.extensions` tuple
72
+ * (`const` keeps it a tuple without `as const` at the call site).
73
+ */
74
+ interface I18nConstructor {
75
+ new <const C extends Config.T<any, any> = Config.T<any, any>>(config?: C): Extension.Piped<I18nCore<Parser.FromConfig<C>, Parser.OutputFromConfig<C>, Schema.FromConfig<C>, Config.LocalesFromConfig<C>>, Extension.FromConfig<C>>;
76
+ }
77
+ declare const I18n: I18nConstructor;
78
+ type I18n<ParserParams extends Parser.Params = any, ParserOutput = string, TranslationSchema = never, LocaleUnion extends string = string> = I18nCore<ParserParams, ParserOutput, TranslationSchema, LocaleUnion>;
79
+ export { I18n };
80
+ export default I18n;