@sveltekit-i18n/base 3.0.0 → 3.0.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 CHANGED
@@ -328,7 +328,7 @@ Load-triggering methods return the promise of the matching load — concurrent d
328
328
  - `setRoute(route)` – update the current route
329
329
  - `loadConfig(config)` – (re)configure the instance
330
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
331
+ - `snapshot()` – serialize the active locale (and the fallback) for the current route, shaped like `config.translations` so the receiving instance hydrates by passing it to `addTranslations()`
332
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
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
334
 
@@ -46,9 +46,14 @@ declare class I18nCore<ParserParams extends Parser.Params = any, ParserOutput =
46
46
  * Serializes what this instance holds for the active locale and the fallback
47
47
  * locale, narrowed to the current route: a key owned only by loaders that do
48
48
  * not match the route is left out. The result is shaped like
49
- * `config.translations`, so a client hydrates by handing it back to the
50
- * constructor — the bookkeeping derived from it then keeps the matching
51
- * loaders from fetching the same data again.
49
+ * `config.translations`, so a client hydrates by passing it to
50
+ * `addTranslations()` — the bookkeeping derived from it then keeps the
51
+ * matching loaders from fetching the same data again. Apply it to the
52
+ * instance rather than assigning it to `config.translations`: the payload is
53
+ * a subset — two locales, and nothing of a key its loaders claim for another
54
+ * route — so assigning it would drop the rest of the config's own data.
55
+ * A literal `__proto__` key is left out: the serializer SvelteKit hands load
56
+ * data to refuses an object that carries one.
52
57
  */
53
58
  snapshot: () => Translations.SerializedTranslations;
54
59
  /**
@@ -1,4 +1,4 @@
1
- import { fetchTranslations, hasOwn, mergeTranslations, read, resolveLoaders, sanitizerFactory, sanitizeTranslationLocales, testRoute, toDotNotation, translate } from './utils.js';
1
+ import { fetchTranslations, hasOwn, mergeTranslations, omitProtoKeys, read, resolveLoaders, sanitizerFactory, sanitizeTranslationLocales, testRoute, toDotNotation, translate } from './utils.js';
2
2
  import { logError, logger, loggerFactory, setLogger } from './logger.js';
3
3
  const defaultCache = Number.POSITIVE_INFINITY;
4
4
  class I18nCore {
@@ -211,9 +211,14 @@ class I18nCore {
211
211
  * Serializes what this instance holds for the active locale and the fallback
212
212
  * locale, narrowed to the current route: a key owned only by loaders that do
213
213
  * not match the route is left out. The result is shaped like
214
- * `config.translations`, so a client hydrates by handing it back to the
215
- * constructor — the bookkeeping derived from it then keeps the matching
216
- * loaders from fetching the same data again.
214
+ * `config.translations`, so a client hydrates by passing it to
215
+ * `addTranslations()` — the bookkeeping derived from it then keeps the
216
+ * matching loaders from fetching the same data again. Apply it to the
217
+ * instance rather than assigning it to `config.translations`: the payload is
218
+ * a subset — two locales, and nothing of a key its loaders claim for another
219
+ * route — so assigning it would drop the rest of the config's own data.
220
+ * A literal `__proto__` key is left out: the serializer SvelteKit hands load
221
+ * data to refuses an object that carries one.
217
222
  */
218
223
  snapshot = () => {
219
224
  const { fallbackLocale } = this.#config ?? {};
@@ -228,9 +233,13 @@ class I18nCore {
228
233
  if (!data)
229
234
  return acc;
230
235
  const offRoute = this.#offRouteKeys(locale, route);
231
- const relevant = Object.keys(data)
236
+ const onRoute = Object.keys(data)
232
237
  .filter((key) => !offRoute.has(key))
233
238
  .reduce((keep, key) => ({ ...keep, [key]: read(data, key) }), {});
239
+ const relevant = omitProtoKeys(onRoute);
240
+ if (relevant !== onRoute) {
241
+ logger.warn(`Leaving a '__proto__' key of locale '${locale}' out of the snapshot: load data cannot carry it.`);
242
+ }
234
243
  // An empty entry would still stamp the locale's freshness on the client,
235
244
  // starting its `cache` window on data it never received.
236
245
  if (!Object.keys(relevant).length)
package/dist/utils.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare const sanitizeTranslationLocales: (input: Translations.Serialized
17
17
  export declare const toDotNotation: DotNotation.T;
18
18
  export declare const resolveLoaders: (input?: readonly Loader.LoaderModule[]) => Loader.LoaderModule[];
19
19
  export declare const mergeTranslations: (target: any, source: any, path: string, onConflict?: (path: string) => void) => any;
20
+ export declare const omitProtoKeys: (value: any) => any;
20
21
  export declare const serialize: (input: Array<Loader.LoaderModule & {
21
22
  data: any;
22
23
  }>) => Translations.SerializedTranslations;
package/dist/utils.js CHANGED
@@ -185,6 +185,27 @@ export const mergeTranslations = (target, source, path, onConflict) => {
185
185
  [key]: hasOwn(acc, key) ? mergeTranslations(read(acc, key), read(source, key), `${path}.${key}`, onConflict) : read(source, key),
186
186
  }), target);
187
187
  };
188
+ const isPlainObject = (value) => {
189
+ if (!value || typeof value !== 'object')
190
+ return false;
191
+ const proto = Object.getPrototypeOf(value);
192
+ return proto === Object.prototype || proto === null;
193
+ };
194
+ // devalue, which SvelteKit serializes load data with, refuses an object with
195
+ // an own '__proto__' key. Branches without one are returned as they are.
196
+ export const omitProtoKeys = (value) => {
197
+ if (Array.isArray(value)) {
198
+ const items = value.map(omitProtoKeys);
199
+ return items.some((item, i) => item !== value[i]) ? items : value;
200
+ }
201
+ if (!isPlainObject(value))
202
+ return value;
203
+ const keys = Object.keys(value);
204
+ const entries = keys.filter((key) => key !== '__proto__').map((key) => [key, omitProtoKeys(value[key])]);
205
+ if (entries.length === keys.length && entries.every(([key, item]) => item === value[key]))
206
+ return value;
207
+ return entries.reduce((acc, [key, item]) => ({ ...acc, [key]: item }), {});
208
+ };
188
209
  const reportLoaderConflict = (path) => {
189
210
  logger.warn(`Conflicting translations for '${path}'. Keeping the value of the last loader.`);
190
211
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltekit-i18n/base",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
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",