@sveltekit-i18n/base 3.1.1 → 3.2.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
@@ -122,8 +122,8 @@ export { load } from '$lib/i18n';
122
122
  The server picks the visitor's locale from the `Accept-Language` header (or
123
123
  from a cookie, with `preferredLocale`), loads it per request and hands it to
124
124
  the browser, so nothing loads twice and no visitor sees another's locale. See
125
- [SvelteKit](./docs/README.md#sveltekit) for the details and
126
- [Server-Side Rendering](./docs/README.md#server-side-rendering) for wiring it
125
+ [SvelteKit](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#sveltekit) for the details and
126
+ [Server-Side Rendering](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#server-side-rendering) for wiring it
127
127
  by hand.
128
128
 
129
129
  ### 4. Use in components
@@ -232,11 +232,11 @@ A named capture group in a `RegExp` route is a load parameter: its match reaches
232
232
  }
233
233
  ```
234
234
 
235
- `API_ORIGIN` stands for an origin such as `https://api.example.com`: a loader runs on the server too, where `fetch` takes only an absolute URL. See [route params](./docs/README.md#route-params) for the rules.
235
+ `API_ORIGIN` stands for an origin such as `https://api.example.com`: a loader runs on the server too, where `fetch` takes only an absolute URL. See [route params](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#route-params) for the rules.
236
236
 
237
- A loader whose source does the caching itself — a SvelteKit remote `query`, an SWR layer, an HTTP cache — sets `cache: false`. It then runs on every load trigger that selects it, and `config.cache` does not apply to it; only a hydrated snapshot holds it back, for the locale and route it was rendered for. See [the loader's `cache`](./docs/README.md#cache-optional).
237
+ A loader whose source does the caching itself — a SvelteKit remote `query`, an SWR layer, an HTTP cache — sets `cache: false`. It then runs on every load trigger that selects it, and `config.cache` does not apply to it; only a hydrated snapshot holds it back, for the locale and route it was rendered for. See [the loader's `cache`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#cache-optional).
238
238
 
239
- A loader that throws is logged, and the rest of the load lands without its data; it runs again on the next load trigger. SvelteKit's `redirect()` and an `error()` below 500 (told by their shape: an own `status` with a `location` or a `body`, on a value that is not an `Error`) are logged too, but they also reject the load, so a SvelteKit `load` awaiting the call hands them to SvelteKit; the rejected call is undone — what it replaced goes back — unless a later call that has not failed came in the meantime. See [the loader](./docs/README.md#loader-required).
239
+ A loader that throws is logged, and the rest of the load lands without its data; it runs again on the next load trigger. SvelteKit's `redirect()` and an `error()` below 500 (told by their shape: an own `status` with a `location` or a `body`, on a value that is not an `Error`) are logged too, but they also reject the load, so a SvelteKit `load` awaiting the call hands them to SvelteKit; the rejected call is undone — what it replaced goes back — unless a later call that has not failed came in the meantime. See [the loader](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#loader-required).
240
240
 
241
241
  Both `loaders` and a loader's `routes` accept readonly arrays, so a whole-config `as const` is fine.
242
242
 
@@ -251,11 +251,11 @@ import { PUBLIC_BASE_PATH } from '$env/static/public';
251
251
  basePath: PUBLIC_BASE_PATH
252
252
  ```
253
253
 
254
- See [`basePath`](./docs/README.md#basepath).
254
+ See [`basePath`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#basepath).
255
255
 
256
256
  ### `translations`
257
257
 
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:
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()`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#hydrateenvelope) instead:
259
259
 
260
260
  ```javascript
261
261
  translations: {
@@ -267,13 +267,13 @@ translations: {
267
267
 
268
268
  ### `initLocale`
269
269
 
270
- Initialize with a specific locale immediately:
270
+ The initial locale, used when nothing else decides:
271
271
 
272
272
  ```javascript
273
273
  initLocale: 'en'
274
274
  ```
275
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.
276
+ With [`defineI18n()`](#sveltekit) it is the locale a visitor gets when nothing they prefer is served, and it loads only when negotiation picks it ([Which locale](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#which-locale)). With `new I18n(config)` the constructor loads it right away, so leave it out of a config whose instance you [`hydrate()`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#hydrateenvelope) by hand — its load starts before the hand-off can be applied.
277
277
 
278
278
  ### `fallbackLocale`
279
279
 
@@ -295,7 +295,7 @@ fallbackValue: '...' // Default: returns the key itself
295
295
 
296
296
  ### `sanitizeLocales`
297
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).
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`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#sanitizelocales).
299
299
 
300
300
  ### `preprocess`
301
301
 
@@ -334,11 +334,11 @@ Or state it per instance — only its type is read, so the value can stay empty
334
334
  const i18n = new I18n({ ...config, schema: {} as TranslationSchema });
335
335
  ```
336
336
 
337
- Hand-write it for a small set of messages, or generate it — [@sveltekit-i18n/typegen](https://github.com/sveltekit-i18n/typegen), a separate package, generates one. A schema whose keys are not a closed set (`schema: {}`) types nothing and keeps the registry out, so keys stay plain strings. A library never registers. The registry needs base 3.1 or newer. See [`schema`](./docs/README.md#schema) for the full rules.
337
+ Hand-write it for a small set of messages, or generate it — [@sveltekit-i18n/typegen](https://github.com/sveltekit-i18n/typegen), a separate package, generates one. A schema whose keys are not a closed set (`schema: {}`) types nothing and keeps the registry out, so keys stay plain strings. A library never registers. The registry needs base 3.1 or newer. See [`schema`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#schema) for the full rules.
338
338
 
339
339
  ### `cache`
340
340
 
341
- Time in milliseconds the loaded translations stay fresh for. By default, loaded translations never expire — each loader (but one with [`cache: false`](./docs/README.md#cache-optional)) runs once per locale and [route params](./docs/README.md#route-params) (a loader's `routes` decide whether a load trigger considers it, and the params their named groups capture decide when it runs again).
341
+ Time in milliseconds the loaded translations stay fresh for. By default, loaded translations never expire — each loader (but one with [`cache: false`](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#cache-optional)) runs once per locale and [route params](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#route-params) (a loader's `routes` decide whether a load trigger considers it, and the params their named groups capture decide when it runs again).
342
342
 
343
343
  Set a finite value when your loaders fetch from a source that can change at runtime (e.g. a CMS):
344
344
 
@@ -399,7 +399,7 @@ log: {
399
399
  - `locales` – available locales
400
400
  - `loading` – `true` while any activating load is in flight; a `{ activate: false }` load counts only once an activating trigger joins it
401
401
  - `initialized` – locale and route set, translations present
402
- - `translations` / `rawTranslations` – the (pre/post-preprocess) tables
402
+ - `translations` / `rawTranslations` – the tables after and before preprocessing
403
403
 
404
404
  ### Methods
405
405
 
@@ -443,12 +443,12 @@ export const { handle, load, use, get } = defineI18n(config, { preferredLocale }
443
443
  - `use(() => data)` – called once in the root `+layout.svelte`; provides the instance, follows every navigation and keeps `<html lang>` and `<html dir>` in sync
444
444
  - `get()` – the instance, in any component below the root layout
445
445
 
446
- Full API documentation: [docs/README.md](./docs/README.md)
446
+ Full API documentation: [docs/README.md](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md)
447
447
 
448
448
  ## Documentation
449
449
 
450
450
  - 🌐 [sveltekit-i18n.github.io](https://sveltekit-i18n.github.io) – The documentation site, with a live playground
451
- - 📖 [Full API Documentation](./docs/README.md) – Complete reference
451
+ - 📖 [Full API Documentation](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md) – Complete reference
452
452
  - 📚 [Main Library Docs](https://github.com/sveltekit-i18n/lib/tree/master/docs/INDEX.md) – Guides, tutorials, and best practices
453
453
  - 🎨 [Parsers](https://github.com/sveltekit-i18n/parsers) – Available parsers and how to create your own
454
454
  - 💡 [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) – Real-world usage examples
@@ -479,7 +479,7 @@ i18n.setLocale('en'); // 'en' | 'de' autocomplete here
479
479
  i18n.setLocale('sv'); // still accepted — the union is a hint, not a constraint
480
480
  ```
481
481
 
482
- 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.
482
+ 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](https://github.com/sveltekit-i18n/base/blob/3.2.0/docs/README.md#typescript) for both.
483
483
 
484
484
  ## Related Packages
485
485
 
@@ -1,5 +1,5 @@
1
1
  import { untrack } from 'svelte';
2
- import { capturesParams, fetchTranslation, hasOwn, loaderName, mergeFetched, mergeTranslations, omitProtoKeys, paramsSignature, read, resolveLoaders, routeParams, sanitizerFactory, sanitizeTranslationLocales, serialize, servedLocales, toDotNotation, translate, unique, withoutBasePath } from './utils.js';
2
+ import { capturesParams, fetchTranslation, hasOwn, loaderName, maskOutputKeys, maskTranslations, mergeFetched, mergeTranslations, omitProtoKeys, paramsSignature, read, resolveLoaders, routeParams, sanitizerFactory, sanitizeTranslationLocales, serialize, servedLocales, toDotNotation, translate, unique, withoutBasePath } from './utils.js';
3
3
  import { logError, logger, loggerFactory, setLogger } from './logger.js';
4
4
  const defaultCache = Number.POSITIVE_INFINITY;
5
5
  /**
@@ -650,6 +650,23 @@ class I18nCore {
650
650
  #addSanitized(sanitized) {
651
651
  this.#mergeTranslations(sanitized);
652
652
  this.#keepExternal(sanitized);
653
+ const { preprocess } = this.#config ?? {};
654
+ // Seeded over what a loader delivered before, so a rebuild of the
655
+ // namespace, which lays the seeds under every delivery, keeps it on top
656
+ // until that loader delivers again.
657
+ this.#deliveries.forEach((delivery, loader) => {
658
+ const own = Object.entries(read(sanitized, loader.locale) ?? {}).filter(([key]) => isNamespaceKey(key, loader.namespace));
659
+ if (!own.length)
660
+ return;
661
+ const nested = read(read(sanitized, loader.locale), loader.namespace);
662
+ const masked = nested === undefined ? delivery.data : maskTranslations(delivery.data, nested) ?? {};
663
+ // Dot notation, as `#preprocess` applies it, merges both spellings of a
664
+ // key into one.
665
+ const dotted = typeof preprocess !== 'function' && preprocess !== 'none'
666
+ ? maskOutputKeys(masked, new Set(Object.keys(toDotNotation(Object.fromEntries(own), preprocess === 'preserveArrays') ?? {})), loader.namespace, preprocess === 'preserveArrays') ?? {}
667
+ : masked;
668
+ this.#deliveries.set(loader, { ...delivery, data: dotted });
669
+ });
653
670
  }
654
671
  /**
655
672
  * Applies plain hand-off data. It records every namespace the data names,
@@ -78,7 +78,9 @@ export const defineI18n = (config, options = {}) => {
78
78
  if (chosen !== undefined)
79
79
  return { locale: chosen, preferred: true };
80
80
  return {
81
- locale: [ranges, ...defaults].reduce((found, candidate) => found ?? matchLocale(candidate, available), undefined),
81
+ // Last, the first locale served: a config that serves one never renders
82
+ // a page without a locale.
83
+ locale: [ranges, ...defaults].reduce((found, candidate) => found ?? matchLocale(candidate, available), undefined) ?? available[0],
82
84
  preferred: false,
83
85
  };
84
86
  };
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. `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.
86
+ * The initial locale. With `defineI18n()` from `/kit`, the locale a visitor gets when neither `preferredLocale` nor what the visitor's browser asks for (`Accept-Language`, or `navigator.languages` without a server `load`) names a locale the config serves; it loads only when negotiation picks it, and without it, or when it matches no locale served, `fallbackLocale` and then the first locale served take that role. With `new I18n(config)`, the constructor loads it right away – leave it out of a config whose instance you `hydrate()` by hand, since its load starts before the hand-off can be applied.
87
87
  */
88
88
  initLocale?: InitLocale;
89
89
  /**
@@ -646,9 +646,11 @@ export declare namespace Translations {
646
646
  type SerializedTranslations = LocaleIndexed<DotNotation.Input>;
647
647
  /**
648
648
  * What `t`/`l` yield. The parser's output on the paths that reach the parser,
649
- * and a plain string on the ones that cannot: an empty key, no locale, or a
650
- * config carrying no parser. `O` is `string` for every parser that returns
651
- * one, which collapses the union everywhere it is not needed.
649
+ * and a plain string on the misses: `''` for an empty key or no locale, the
650
+ * key itself for a missing translation. Two paths hand back data unchecked:
651
+ * a configured `fallbackValue`, and the stored value while the config
652
+ * carries no parser. `O` is `string` for every parser that returns one,
653
+ * which collapses the union everywhere it is not needed.
652
654
  */
653
655
  type Translated<O> = O | string;
654
656
  type TranslationFunction<P extends Parser.Params = Parser.Params, O = string, S = never> = <K extends Schema.Key<S>>(key: K, ...restParams: Schema.Params<S, K, P>) => Translated<O>;
package/dist/utils.d.ts CHANGED
@@ -54,6 +54,8 @@ export declare const toDotNotation: DotNotation.T;
54
54
  export declare const unique: <V>(values: readonly V[]) => V[];
55
55
  export declare const resolveLoaders: (input?: readonly Loader.LoaderModule[], sanitizeLocales?: Config.SanitizeLocales) => Loader.Resolved[];
56
56
  export declare const mergeTranslations: (target: any, source: any, path: string, onConflict?: (path: string) => void) => any;
57
+ export declare const maskTranslations: (target: any, source: any) => any;
58
+ export declare const maskOutputKeys: (target: any, keys: ReadonlySet<string>, prefix: string, preserveArrays?: boolean) => any;
57
59
  export declare const omitProtoKeys: (value: any) => any;
58
60
  export declare const serialize: (input: Array<Loader.Resolved & {
59
61
  data: any;
package/dist/utils.js CHANGED
@@ -492,6 +492,42 @@ export const mergeTranslations = (target, source, path, onConflict) => {
492
492
  [key]: hasOwn(acc, key) ? mergeTranslations(read(acc, key), read(source, key), `${path}.${key}`, onConflict) : read(source, key),
493
493
  }), target);
494
494
  };
495
+ // What of `target` still shows once `source` is merged over it: the branches
496
+ // both hold as objects keep what `source` leaves out, and anything else it
497
+ // sets is gone. `undefined` when `source` replaces the whole of `target`.
498
+ export const maskTranslations = (target, source) => {
499
+ if (!isMergeable(target) || !isMergeable(source))
500
+ return undefined;
501
+ return Object.keys(target).reduce((acc, key) => {
502
+ if (!hasOwn(source, key))
503
+ return { ...acc, [key]: read(target, key) };
504
+ const masked = maskTranslations(read(target, key), read(source, key));
505
+ return masked === undefined ? acc : { ...acc, [key]: masked };
506
+ }, {});
507
+ };
508
+ // What of `target`, dot-notated under `prefix`, still shows once `keys` are
509
+ // dot-notated beside it: a leaf whose key is among them is gone. A key spelled
510
+ // with dots and a nested branch meet only there, after `preprocess`. Unless
511
+ // `preserveArrays`, an array is walked as `toDotNotation` walks it, and one
512
+ // that lost an item keeps the others under their indexes. What lost nothing is
513
+ // returned as it is, and an array that lost no item and holds nothing but its
514
+ // items stays one.
515
+ export const maskOutputKeys = (target, keys, prefix, preserveArrays = false) => {
516
+ const walked = Array.isArray(target) ? !preserveArrays : isMergeable(target);
517
+ if (!walked)
518
+ return keys.has(prefix) ? undefined : target;
519
+ const entries = Object.keys(target).flatMap((key) => {
520
+ const masked = maskOutputKeys(read(target, key), keys, `${prefix}.${key}`, preserveArrays);
521
+ return masked === undefined ? [] : [[key, masked]];
522
+ });
523
+ if (entries.length === Object.keys(target).length) {
524
+ if (entries.every(([key, value]) => Object.is(value, read(target, key))))
525
+ return target;
526
+ if (Array.isArray(target) && entries.every(([key], index) => key === `${index}`))
527
+ return entries.map(([, value]) => value);
528
+ }
529
+ return entries.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
530
+ };
495
531
  const isPlainObject = (value) => {
496
532
  if (!value || typeof value !== 'object')
497
533
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltekit-i18n/base",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Base functionality of sveltekit-i18n library with a support for external message parsers.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",