@sveltekit-i18n/base 3.1.0 → 3.1.2

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.1.2/docs/README.md#sveltekit) for the details and
126
+ [Server-Side Rendering](https://github.com/sveltekit-i18n/base/blob/3.1.2/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.1.2/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.1.2/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.1.2/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.1.2/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.1.2/docs/README.md#hydrateenvelope) instead:
259
259
 
260
260
  ```javascript
261
261
  translations: {
@@ -273,7 +273,7 @@ Initialize with a specific locale immediately:
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 loads nothing: it is a negotiation candidate. Leave it out of a config whose instance you [`hydrate()`](https://github.com/sveltekit-i18n/base/blob/3.1.2/docs/README.md#hydrateenvelope) by hand — its load starts in the constructor, 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.1.2/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.1.2/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.1.2/docs/README.md#cache-optional)) runs once per locale and [route params](https://github.com/sveltekit-i18n/base/blob/3.1.2/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.1.2/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.1.2/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.1.2/docs/README.md#typescript) for both.
483
483
 
484
484
  ## Related Packages
485
485
 
@@ -113,9 +113,10 @@ declare class I18nCore<ParserParams extends Parser.Params = any, ParserOutput =
113
113
  * records name each loader, so a namespace fed by several loaders is handed
114
114
  * over too, and so is one a loader delivered for route params while its
115
115
  * record says so — not a namespace with both, whose data the client could
116
- * not split between them. What was seeded into the namespace of a loader
117
- * whose routes capture params travels apart as `seeds`, so it outlives new
118
- * params there, and reaches the client where the namespace is left out.
116
+ * not split between them. What was seeded into the namespace of a recorded
117
+ * loader, or of one whose routes capture params, travels apart as `seeds`,
118
+ * so it outlives the loader's next fetch there, and reaches the client
119
+ * where the namespace is left out.
119
120
  */
120
121
  snapshot: {
121
122
  (options?: {
@@ -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
  /**
@@ -413,9 +413,10 @@ class I18nCore {
413
413
  * records name each loader, so a namespace fed by several loaders is handed
414
414
  * over too, and so is one a loader delivered for route params while its
415
415
  * record says so — not a namespace with both, whose data the client could
416
- * not split between them. What was seeded into the namespace of a loader
417
- * whose routes capture params travels apart as `seeds`, so it outlives new
418
- * params there, and reaches the client where the namespace is left out.
416
+ * not split between them. What was seeded into the namespace of a recorded
417
+ * loader, or of one whose routes capture params, travels apart as `seeds`,
418
+ * so it outlives the loader's next fetch there, and reaches the client
419
+ * where the namespace is left out.
419
420
  */
420
421
  snapshot = ((options) => {
421
422
  const withRecords = options?.records === true;
@@ -472,11 +473,12 @@ class I18nCore {
472
473
  return [];
473
474
  return [signature ? { id, signature } : { id }];
474
475
  });
475
- // Where params can change, the client takes a recorded namespace whole as
476
- // its loader's delivery and loads one left out itself, so what was seeded
477
- // into it travels apart: the seed has to outlive the delivery new params
478
- // replace there as it does here.
479
- const seeds = omitProtoKeys(this.#externalOf(loaders.filter(({ locale, routes }) => omitted.has(locale) && locale !== '__proto__' && capturesParams(routes))));
476
+ // The client takes a recorded namespace whole as its loader's delivery,
477
+ // and loads one left out where params can change itself, so what was
478
+ // seeded into either travels apart: the seed has to outlive the delivery
479
+ // the next fetch replaces there as it does here.
480
+ const recorded = new Set(records.map(({ id }) => id));
481
+ const seeds = omitProtoKeys(this.#externalOf(loaders.filter(({ id, locale, routes }) => omitted.has(locale) && locale !== '__proto__' && (capturesParams(routes) || (id !== null && recorded.has(id))))));
480
482
  return {
481
483
  translations,
482
484
  records,
@@ -585,34 +587,40 @@ class I18nCore {
585
587
  });
586
588
  const takers = new Map([...byIdTaken, ...byFunction]);
587
589
  const orphaned = deliveries.filter((delivery) => !takers.has(delivery));
588
- this.#deliveries = new Map(Array.from(takers, ([delivery, taker]) => [taker, { ...delivery, loader: taker }]));
590
+ this.#deliveries = new Map(deliveries.flatMap((delivery) => {
591
+ const taker = takers.get(delivery);
592
+ return taker ? [[taker, { ...delivery, loader: taker }]] : [];
593
+ }));
589
594
  this.#keepExternal(serialize(orphaned.map(({ loader, data }) => ({ ...loader, data }))));
590
595
  }
591
596
  /**
592
- * Applies what loaders delivered and records them as loaded. A loader whose
593
- * params changed replaces the part of its namespace it delivered before:
594
- * the namespace is rebuilt from the data supplied without a loader and from
595
- * what each of its loaders last delivered, so no key of the previous params
596
- * survives and a sibling's part stays in place. The preprocessed table of a
597
+ * Applies what loaders delivered and records them as loaded. A loader that
598
+ * delivered before, for the same params or for others, replaces the part of
599
+ * its namespace it delivered then: the namespace is rebuilt from the data
600
+ * supplied without a loader and from what each of its loaders last
601
+ * delivered, so no key its source dropped survives and a sibling's part
602
+ * stays in place. The preprocessed table of a
597
603
  * locale that lost data is derived again from the raw one, since a custom
598
604
  * `preprocess` may have renamed the keys that would have to go. Both tables
599
605
  * are computed before anything is written, so a `preprocess` that throws
600
606
  * records no loader and the next trigger fetches it again.
601
607
  */
602
- #applyDeliveries(deliveries) {
608
+ #applyDeliveries(applied) {
609
+ const { loaders = [] } = this.#config ?? {};
610
+ // One load, however its deliveries were gathered: a preload claimed with
611
+ // it comes first, yet the loader declared later still wins.
612
+ const deliveries = [...applied].sort((a, b) => loaders.indexOf(a.loader) - loaders.indexOf(b.loader));
603
613
  const replaced = deliveries
604
- .filter(({ loader, signature }) => {
605
- const previous = this.#deliveries.get(loader);
606
- return previous !== undefined && previous.signature !== signature;
607
- })
614
+ .filter(({ loader }) => this.#deliveries.has(loader))
608
615
  .map(({ loader }) => loader);
609
- const delivered = new Map(deliveries.map((delivery) => [delivery.loader, delivery]));
616
+ const delivered = new Set(deliveries.map(({ loader }) => loader));
610
617
  const isReplaced = ({ locale, namespace }) => replaced.some((loader) => loader.locale === locale && loader.namespace === namespace);
611
- const { loaders = [] } = this.#config ?? {};
612
- const rebuilt = loaders
613
- .filter(isReplaced)
614
- .map((loader) => delivered.get(loader) ?? this.#deliveries.get(loader))
615
- .filter((delivery) => delivery !== undefined);
618
+ // In the order they were delivered, so across loads the data delivered
619
+ // last wins, as it does where nothing is rebuilt.
620
+ const rebuilt = [
621
+ ...Array.from(this.#deliveries.values()).filter(({ loader }) => isReplaced(loader) && !delivered.has(loader)),
622
+ ...deliveries.filter(({ loader }) => isReplaced(loader)),
623
+ ];
616
624
  const raw = replaced.reduce((acc, { locale, namespace }) => ({
617
625
  ...acc,
618
626
  [locale]: Object.fromEntries(Object.entries(read(acc, locale) ?? {}).filter(([key]) => !isNamespaceKey(key, namespace))),
@@ -642,6 +650,23 @@ class I18nCore {
642
650
  #addSanitized(sanitized) {
643
651
  this.#mergeTranslations(sanitized);
644
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
+ });
645
670
  }
646
671
  /**
647
672
  * Applies plain hand-off data. It records every namespace the data names,
@@ -672,7 +697,7 @@ class I18nCore {
672
697
  * that change later replace it; the rest is kept as data supplied without a
673
698
  * loader and, like a seed, records no namespace — the hand-off says which loaders it
674
699
  * covers, and anything else loads again rather than going missing. `seeds`
675
- * is what was seeded where params can change, displayed and kept as a seed
700
+ * is what was seeded into those namespaces, displayed and kept as a seed
676
701
  * so that it outlives a delivery. A record naming no loader of this config
677
702
  * is dropped, and its loader runs again.
678
703
  */
@@ -705,6 +730,8 @@ class I18nCore {
705
730
  }
706
731
  /** Records `delivery` as its loader's; what was parked for its params is older. */
707
732
  #record(delivery) {
733
+ // Deleted first, so the map keeps the order the deliveries arrived in.
734
+ this.#deliveries.delete(delivery.loader);
708
735
  this.#deliveries.set(delivery.loader, delivery);
709
736
  this.#loaderRecords.set(delivery.loader, delivery.signature);
710
737
  if (this.#parked.get(delivery.loader)?.signature === delivery.signature)
package/dist/types.d.ts CHANGED
@@ -628,10 +628,11 @@ export declare namespace Snapshot {
628
628
  */
629
629
  records?: LoadRecord[];
630
630
  /**
631
- * What was seeded into the namespace of a loader whose routes capture
632
- * params – repeated where `translations` carries the namespace, and
633
- * carried where the payload leaves it out, so that it outlives the
634
- * delivery new params replace. Read with `records` only.
631
+ * What was seeded into the namespace of a recorded loader or of one whose
632
+ * routes capture params – repeated where `translations` carries the
633
+ * namespace, and carried where the payload leaves it out, so that it
634
+ * outlives the delivery the loader's next fetch replaces. Read with
635
+ * `records` only.
635
636
  */
636
637
  seeds?: Translations.SerializedTranslations;
637
638
  /** The active locale. */
@@ -645,9 +646,11 @@ export declare namespace Translations {
645
646
  type SerializedTranslations = LocaleIndexed<DotNotation.Input>;
646
647
  /**
647
648
  * What `t`/`l` yield. The parser's output on the paths that reach the parser,
648
- * and a plain string on the ones that cannot: an empty key, no locale, or a
649
- * config carrying no parser. `O` is `string` for every parser that returns
650
- * 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.
651
654
  */
652
655
  type Translated<O> = O | string;
653
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.0",
3
+ "version": "3.1.2",
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",