@sveltekit-i18n/base 3.1.0-next.0 → 3.1.0-next.1
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 +35 -10
- package/dist/kit/define.svelte.js +14 -6
- package/dist/kit/internal.d.ts +6 -1
- package/dist/kit/server.js +3 -3
- package/dist/kit/types.d.ts +11 -1
- package/dist/types.d.ts +9 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ Core i18n functionality for SvelteKit with support for custom message parsers. T
|
|
|
34
34
|
|
|
35
35
|
Svelte 5 or newer, and one of Node 22+, Bun 1.2+ or Deno 2+. The package is
|
|
36
36
|
ESM-only and imports no `node:` module, so every runtime that runs your
|
|
37
|
-
SvelteKit build runs it.
|
|
37
|
+
SvelteKit build runs it. The [`/kit`](#sveltekit) subpath needs SvelteKit 2;
|
|
38
|
+
the SvelteKit behaviour the docs describe is checked against 2.70 and the 3.0
|
|
39
|
+
prerelease.
|
|
38
40
|
|
|
39
41
|
## Installation
|
|
40
42
|
|
|
@@ -171,8 +173,10 @@ import i18n from '@sveltekit-i18n/base';
|
|
|
171
173
|
|
|
172
174
|
const customParser = () => ({
|
|
173
175
|
parse: (value, params) => {
|
|
174
|
-
// Your custom interpolation logic
|
|
175
|
-
return value
|
|
176
|
+
// Your custom interpolation logic; `parse` must not throw on a non-string value
|
|
177
|
+
return typeof value === 'string'
|
|
178
|
+
? value.replace(/\{(\w+)\}/g, (_, key) => params[0]?.[key] ?? key)
|
|
179
|
+
: value;
|
|
176
180
|
},
|
|
177
181
|
});
|
|
178
182
|
|
|
@@ -236,6 +240,19 @@ A loader that throws is logged, and the rest of the load lands without its data;
|
|
|
236
240
|
|
|
237
241
|
Both `loaders` and a loader's `routes` accept readonly arrays, so a whole-config `as const` is fine.
|
|
238
242
|
|
|
243
|
+
### `basePath`
|
|
244
|
+
|
|
245
|
+
The path the app is served under — SvelteKit's `kit.paths.base`. Every route handed in loses it on the way in, on a segment boundary only (under `/repo`, `/repo/about` is `/about`), so loader `routes` name the app's own paths. Set both from one environment variable:
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
// svelte.config.js: kit: { paths: { base: process.env.PUBLIC_BASE_PATH ?? '' } }
|
|
249
|
+
import { PUBLIC_BASE_PATH } from '$env/static/public';
|
|
250
|
+
|
|
251
|
+
basePath: PUBLIC_BASE_PATH
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
See [`basePath`](./docs/README.md#basepath).
|
|
255
|
+
|
|
239
256
|
### `translations`
|
|
240
257
|
|
|
241
258
|
Synchronous translations, available immediately. They seed the tables: the loaders of a namespace they name still run and merge into it. Hand a server's state over with [`hydrate()`](./docs/README.md#hydrateenvelope) instead:
|
|
@@ -256,6 +273,8 @@ Initialize with a specific locale immediately:
|
|
|
256
273
|
initLocale: 'en'
|
|
257
274
|
```
|
|
258
275
|
|
|
276
|
+
With [`defineI18n()`](#sveltekit) it loads nothing: it is a negotiation candidate. Leave it out of a config whose instance you [`hydrate()`](./docs/README.md#hydrateenvelope) by hand — its load starts in the constructor, before the hand-off can be applied.
|
|
277
|
+
|
|
259
278
|
### `fallbackLocale`
|
|
260
279
|
|
|
261
280
|
Fallback when translation is missing:
|
|
@@ -274,6 +293,10 @@ Default return value when translation key is not found:
|
|
|
274
293
|
fallbackValue: '...' // Default: returns the key itself
|
|
275
294
|
```
|
|
276
295
|
|
|
296
|
+
### `sanitizeLocales`
|
|
297
|
+
|
|
298
|
+
How locale identifiers are normalized before they key anything: `true` (default) resolves them to their ISO form through `Intl` (`'en-us'` is `'en-US'`), `false` keeps them as authored, and a function normalizes them your way. See [`sanitizeLocales`](./docs/README.md#sanitizelocales).
|
|
299
|
+
|
|
277
300
|
### `preprocess`
|
|
278
301
|
|
|
279
302
|
Transform translations after loading:
|
|
@@ -300,7 +323,7 @@ type TranslationSchema = {
|
|
|
300
323
|
const i18n = new I18n({ ...config, schema: {} as TranslationSchema });
|
|
301
324
|
```
|
|
302
325
|
|
|
303
|
-
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.
|
|
326
|
+
Hand-write it for a small set of messages, or point the slot at a generated artifact — [@sveltekit-i18n/typegen](https://github.com/sveltekit-i18n/typegen), a separate package, generates one. 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.
|
|
304
327
|
|
|
305
328
|
### `cache`
|
|
306
329
|
|
|
@@ -309,10 +332,10 @@ Time in milliseconds the loaded translations stay fresh for. By default, loaded
|
|
|
309
332
|
Set a finite value when your loaders fetch from a source that can change at runtime (e.g. a CMS):
|
|
310
333
|
|
|
311
334
|
```javascript
|
|
312
|
-
cache: 3600000 // Translations older than 1 hour refetch on the next load
|
|
335
|
+
cache: 3600000 // Translations older than 1 hour refetch on the next activating load
|
|
313
336
|
```
|
|
314
337
|
|
|
315
|
-
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).
|
|
338
|
+
Expiry is evaluated by the next activating load trigger (`setLocale`, `setRoute`, `loadTranslations`); a warm load — `loadTranslations(…, { activate: false })` or `loadNamespace()` — fills the tables without evaluating it. Set to `0` to treat translations as always stale (refetch on every activating load trigger). A loader with `cache: false` is outside the window. You can also drop the loaded state manually at any time with [`invalidate()`](#methods).
|
|
316
339
|
|
|
317
340
|
### `extensions`
|
|
318
341
|
|
|
@@ -423,11 +446,12 @@ Full API documentation: [docs/README.md](./docs/README.md)
|
|
|
423
446
|
|
|
424
447
|
```typescript
|
|
425
448
|
import { I18n, type Config } from '@sveltekit-i18n/base';
|
|
426
|
-
import parser from '@sveltekit-i18n/parser-curly';
|
|
449
|
+
import parser, { type Parser } from '@sveltekit-i18n/parser-curly';
|
|
427
450
|
|
|
428
451
|
// The parser's params – the rest parameters of `t`/`l`. Annotate only when the
|
|
429
|
-
// config lives on its own; `new I18n({ ... })` infers them.
|
|
430
|
-
|
|
452
|
+
// config lives on its own; `new I18n({ ... })` infers them. Take the tuple from
|
|
453
|
+
// the parser rather than spelling it by hand.
|
|
454
|
+
type Params = Parser.Params;
|
|
431
455
|
|
|
432
456
|
const config: Config.T<Params> = {
|
|
433
457
|
parser: parser({ onReport: null }),
|
|
@@ -435,7 +459,7 @@ const config: Config.T<Params> = {
|
|
|
435
459
|
};
|
|
436
460
|
```
|
|
437
461
|
|
|
438
|
-
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`):
|
|
462
|
+
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`, `loadNamespace`, `invalidate`, `l`, `locale`, `locales`):
|
|
439
463
|
|
|
440
464
|
```typescript
|
|
441
465
|
const i18n = new I18n({ parser: parser({ onReport: null }), initLocale: 'en', fallbackLocale: 'de' });
|
|
@@ -454,6 +478,7 @@ The locales survive only when the config reaches the constructor as a literal
|
|
|
454
478
|
- [@sveltekit-i18n/parser-mf2](https://github.com/sveltekit-i18n/parsers/tree/master/parser-mf2) – [Unicode MessageFormat 2](https://unicode.org/reports/tr35/tr35-messageFormat.html) parser
|
|
455
479
|
- [@sveltekit-i18n/parser-i18next](https://github.com/sveltekit-i18n/parsers/tree/master/parser-i18next) – [i18next](https://www.i18next.com) interpolation and formatting syntax parser
|
|
456
480
|
- [Extensions](https://github.com/sveltekit-i18n/extensions) – Official extensions for the `config.extensions` pipe
|
|
481
|
+
- [@sveltekit-i18n/typegen](https://github.com/sveltekit-i18n/typegen) – Generates the [`schema`](#schema) type from your translation files
|
|
457
482
|
|
|
458
483
|
## Contributing
|
|
459
484
|
|
|
@@ -74,7 +74,13 @@ export const defineI18n = (config, options = {}) => {
|
|
|
74
74
|
const negotiate = (event, ranges) => {
|
|
75
75
|
// First: it sets the config's logger, which `preferred` reports through.
|
|
76
76
|
const available = locales();
|
|
77
|
-
|
|
77
|
+
const chosen = matchLocale(visitorSanitized(preferred(event)), available);
|
|
78
|
+
if (chosen !== undefined)
|
|
79
|
+
return { locale: chosen, preferred: true };
|
|
80
|
+
return {
|
|
81
|
+
locale: [ranges, ...defaults].reduce((found, candidate) => found ?? matchLocale(candidate, available), undefined),
|
|
82
|
+
preferred: false,
|
|
83
|
+
};
|
|
78
84
|
};
|
|
79
85
|
const server = serverHalf({ create, negotiate, locales, basePath: config.basePath });
|
|
80
86
|
// Browser only: the tab's instance, the server's answer at the last commit,
|
|
@@ -96,11 +102,13 @@ export const defineI18n = (config, options = {}) => {
|
|
|
96
102
|
const seen = heading(i18n);
|
|
97
103
|
// A live server sends the tables on a page render only, so a later pass
|
|
98
104
|
// that carries them read a prerendered file, whose locale was negotiated
|
|
99
|
-
// at build time, without the visitor
|
|
100
|
-
// `
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// at build time, without the visitor: it counts only when the build's
|
|
106
|
+
// `preferredLocale` gave it. Node and Deno define `navigator.languages`
|
|
107
|
+
// too, from the server's own environment.
|
|
108
|
+
const prerendered = !fresh && payload?.translations;
|
|
109
|
+
const answer = prerendered
|
|
110
|
+
? (payload.preferred ? payload.locale : tab.answer)
|
|
111
|
+
: payload ? payload.locale : negotiate(event, BROWSER ? navigator.languages : undefined).locale;
|
|
104
112
|
if (BROWSER)
|
|
105
113
|
Object.assign(tab, { i18n, surface });
|
|
106
114
|
if (fresh) {
|
package/dist/kit/internal.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { I18n } from '../I18n.svelte.js';
|
|
2
2
|
import type { Kit } from './types.js';
|
|
3
|
+
/** The negotiated locale, and whether `preferredLocale` gave it. */
|
|
4
|
+
export type Negotiated = {
|
|
5
|
+
locale: string | undefined;
|
|
6
|
+
preferred: boolean;
|
|
7
|
+
};
|
|
3
8
|
/** What the server half needs from the factory. */
|
|
4
9
|
export type Shared = {
|
|
5
10
|
create: () => I18n;
|
|
6
|
-
negotiate: (event: Kit.Event, ranges: string | readonly string[] | null | undefined) =>
|
|
11
|
+
negotiate: (event: Kit.Event, ranges: string | readonly string[] | null | undefined) => Negotiated;
|
|
7
12
|
/** The locales the config serves. */
|
|
8
13
|
locales: () => string[];
|
|
9
14
|
basePath: string | undefined;
|
package/dist/kit/server.js
CHANGED
|
@@ -32,7 +32,7 @@ export const serverHalf = ({ create, negotiate, locales, basePath }) => {
|
|
|
32
32
|
const negotiated = () => {
|
|
33
33
|
if (lang === undefined) {
|
|
34
34
|
check(event);
|
|
35
|
-
lang = answer(event) ?? '';
|
|
35
|
+
lang = answer(event).locale ?? '';
|
|
36
36
|
}
|
|
37
37
|
return lang;
|
|
38
38
|
};
|
|
@@ -61,12 +61,12 @@ export const serverHalf = ({ create, negotiate, locales, basePath }) => {
|
|
|
61
61
|
// navigation only for what it read.
|
|
62
62
|
const route = event.url.pathname;
|
|
63
63
|
check(event);
|
|
64
|
-
const locale = answer(event);
|
|
64
|
+
const { locale, preferred } = answer(event);
|
|
65
65
|
if (event.isDataRequest)
|
|
66
66
|
return { i18n: { locale, route: withoutBasePath(route, basePath) } };
|
|
67
67
|
const i18n = create();
|
|
68
68
|
await (locale ? i18n.loadTranslations(locale, route) : i18n.setRoute(route));
|
|
69
|
-
return { i18n: i18n.snapshot({ records: true }) };
|
|
69
|
+
return { i18n: { ...i18n.snapshot({ records: true }), ...(preferred ? { preferred: true } : {}) } };
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
};
|
package/dist/kit/types.d.ts
CHANGED
|
@@ -37,7 +37,14 @@ export declare namespace Kit {
|
|
|
37
37
|
* locale and the route always, the tables and their records on a page
|
|
38
38
|
* render only. Plain data, for `devalue`.
|
|
39
39
|
*/
|
|
40
|
-
type Payload = Omit<Snapshot.Envelope, 'translations'> & Partial<Pick<Snapshot.Envelope, 'translations'
|
|
40
|
+
type Payload = Omit<Snapshot.Envelope, 'translations'> & Partial<Pick<Snapshot.Envelope, 'translations'>> & {
|
|
41
|
+
/**
|
|
42
|
+
* Set on a page render when `preferredLocale` gave the locale. A
|
|
43
|
+
* prerendered page's data is that render, so a navigation to one takes
|
|
44
|
+
* its locale only when the build's `preferredLocale` gave it.
|
|
45
|
+
*/
|
|
46
|
+
preferred?: true;
|
|
47
|
+
};
|
|
41
48
|
type Options = {
|
|
42
49
|
/**
|
|
43
50
|
* The visitor's choice, read from the event: a cookie, a route param, a
|
|
@@ -46,6 +53,9 @@ export declare namespace Kit {
|
|
|
46
53
|
* locale matches is skipped; a custom `sanitizeLocales` is applied to it
|
|
47
54
|
* first. It runs on every navigation and every
|
|
48
55
|
* preload, so it must be pure: it reads the event and writes nothing.
|
|
56
|
+
* With a server load it runs on the server only: a navigation to a
|
|
57
|
+
* prerendered page takes the locale it gave at build time, and otherwise
|
|
58
|
+
* keeps the tab's.
|
|
49
59
|
*
|
|
50
60
|
* @example
|
|
51
61
|
* preferredLocale: (event) => event.cookies?.get('lang')
|
package/dist/types.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export declare namespace Config {
|
|
|
83
83
|
*/
|
|
84
84
|
translations?: Translations.T;
|
|
85
85
|
/**
|
|
86
|
-
* If you set this property, translations will be initialized immediately using this locale.
|
|
86
|
+
* If you set this property, translations will be initialized immediately using this locale. `defineI18n()` from `/kit` loads nothing for it: there it is a negotiation candidate. Leave it out of a config whose instance you `hydrate()` by hand – its load starts in the constructor, before the hand-off can be applied.
|
|
87
87
|
*/
|
|
88
88
|
initLocale?: InitLocale;
|
|
89
89
|
/**
|
|
@@ -136,8 +136,9 @@ export declare namespace Config {
|
|
|
136
136
|
* `export const schema = {} as TranslationSchema`. A schema whose keys are not a
|
|
137
137
|
* closed set (an open index signature, or no keys at all) is ignored and
|
|
138
138
|
* keys stay plain strings. Read at construction time only: a later
|
|
139
|
-
* `loadConfig()` cannot retype the instance, and
|
|
140
|
-
* erases the instance's type parameters
|
|
139
|
+
* `loadConfig()` cannot retype the instance, and an extension typed by a
|
|
140
|
+
* fixed return type erases the instance's type parameters, while one
|
|
141
|
+
* typed by an `Extension.Operator` (`Extension.Generic`) keeps them.
|
|
141
142
|
*
|
|
142
143
|
* @example
|
|
143
144
|
* import { schema } from './generated/i18n-schema.js';
|
|
@@ -162,11 +163,11 @@ export declare namespace Config {
|
|
|
162
163
|
*/
|
|
163
164
|
basePath?: string;
|
|
164
165
|
/**
|
|
165
|
-
* Time in milliseconds the loaded translations stay fresh for. Once a locale's translations are older, the next load trigger runs its loaders again. By default, loaded translations never expire – call `invalidate()` (or set a finite `cache`) when your translation source can change at runtime, e.g. a CMS.
|
|
166
|
+
* Time in milliseconds the loaded translations stay fresh for. Once a locale's translations are older, the next activating load trigger (`setLocale()`, `setRoute()`, `loadTranslations()`) runs its loaders again; a warm load – `loadTranslations(…, { activate: false })` or `loadNamespace()` – fills the tables without evaluating the window. By default, loaded translations never expire – call `invalidate()` (or set a finite `cache`) when your translation source can change at runtime, e.g. a CMS. A loader with `cache: false` is outside the window.
|
|
166
167
|
*
|
|
167
168
|
* @default Number.POSITIVE_INFINITY
|
|
168
169
|
*
|
|
169
|
-
* @tip Set to `0` to treat translations as always stale (refetch on every load trigger).
|
|
170
|
+
* @tip Set to `0` to treat translations as always stale (refetch on every activating load trigger).
|
|
170
171
|
*/
|
|
171
172
|
cache?: number;
|
|
172
173
|
/**
|
|
@@ -442,7 +443,7 @@ export declare namespace Parser {
|
|
|
442
443
|
* with anything yields the other kind. `'date'` covers both date and time
|
|
443
444
|
* formatting and means `Date | number`. `'function'` is a rich-text callback,
|
|
444
445
|
* the shape ICU tags require. `'boolean'` is here for parsers that can prove
|
|
445
|
-
* it –
|
|
446
|
+
* it – no official parser can, since each compares stringified values.
|
|
446
447
|
*/
|
|
447
448
|
type ParamKind = 'unknown' | 'string' | 'number' | 'boolean' | 'date' | 'function';
|
|
448
449
|
/**
|
|
@@ -462,7 +463,7 @@ export declare namespace Parser {
|
|
|
462
463
|
kind?: ParamKind | readonly ParamKind[];
|
|
463
464
|
/**
|
|
464
465
|
* Values the message names explicitly – a hint for authoring tools, never
|
|
465
|
-
* an exhaustive set.
|
|
466
|
+
* an exhaustive set. Every official parser falls back to a default branch
|
|
466
467
|
* for anything unlisted, so this must not be used to close a union. Omit it
|
|
467
468
|
* where the listed values are not values at all (numeric thresholds, plural
|
|
468
469
|
* categories) or mean the opposite (an inequality's operands).
|
|
@@ -485,7 +486,7 @@ export declare namespace Parser {
|
|
|
485
486
|
branch: string;
|
|
486
487
|
}[];
|
|
487
488
|
};
|
|
488
|
-
/** Diagnostic context for an extractor.
|
|
489
|
+
/** Diagnostic context for an extractor. No official parser needs it to extract. */
|
|
489
490
|
type ExtractContext = {
|
|
490
491
|
key?: Key;
|
|
491
492
|
locale?: Locale;
|
package/package.json
CHANGED