@sveltekit-i18n/base 3.1.2 → 3.3.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/dist/utils.js CHANGED
@@ -6,7 +6,7 @@ export const hasOwn = (obj, key) => obj != null && Object.prototype.hasOwnProper
6
6
  // Own-property read: returns the value only when `key` is the object's own
7
7
  // property, otherwise undefined. Centralizes the prototype-safe table lookup.
8
8
  export const read = (obj, key) => (hasOwn(obj, key) ? obj[key] : undefined);
9
- export const translate = ({ parser, key, params, translations, locale, fallbackLocale, ...rest }) => {
9
+ export const translate = (config, locale, key, params, table, fallbackTable) => {
10
10
  if (!key) {
11
11
  logger.warn(`No translation key provided ('${locale}' locale). Skipping translation...`);
12
12
  return '';
@@ -15,23 +15,23 @@ export const translate = ({ parser, key, params, translations, locale, fallbackL
15
15
  logger.warn(`No locale provided for '${key}' key. Skipping translation...`);
16
16
  return '';
17
17
  }
18
- const localeTranslations = read(translations, locale);
19
- let text = read(localeTranslations, key);
18
+ const fallbackLocale = config?.fallbackLocale;
19
+ let text = read(table, key);
20
20
  if (fallbackLocale && text === undefined) {
21
21
  logger.debug(`No translation provided for '${key}' key in locale '${locale}'. Trying fallback '${fallbackLocale}'`);
22
- const fallbackTranslations = read(translations, fallbackLocale);
23
- text = read(fallbackTranslations, key);
22
+ text = read(fallbackTable, key);
24
23
  }
25
24
  if (text === undefined) {
26
25
  logger.debug(`No translation provided for '${key}' key in fallback '${fallbackLocale}'.`);
27
- if (hasOwn(rest, 'fallbackValue')) {
28
- return rest.fallbackValue;
26
+ if (hasOwn(config, 'fallbackValue')) {
27
+ return config?.fallbackValue;
29
28
  }
30
29
  logger.warn(`No translation nor fallback found for '${key}' .`);
31
30
  // There is nothing to interpolate, so no parser is asked to. Echoing the
32
31
  // key is what makes a missing translation visible instead of blank.
33
32
  return key;
34
33
  }
34
+ const parser = config?.parser;
35
35
  if (!parser || typeof parser.parse !== 'function') {
36
36
  // Reached on every call while no parser is set (e.g. before config loads),
37
37
  // so keep it at debug to avoid flooding logs on the render path.
@@ -43,7 +43,10 @@ export const translate = ({ parser, key, params, translations, locale, fallbackL
43
43
  return parser.parse(text, params, locale, key);
44
44
  };
45
45
  // `Intl.Collator.supportedLocalesOf` is comparatively expensive and locales
46
- // repeat constantly — per loader on every load trigger, per lookup.
46
+ // repeat constantly — per loader on every load trigger, per lookup. A locale
47
+ // Intl rejects is remembered too: its data is fixed for the life of the realm,
48
+ // as the remembered hits already assume. It is still reported on every call,
49
+ // so the warning reaches whichever logger and level is installed at the time.
47
50
  const LOCALE_CACHE_LIMIT = 1000;
48
51
  const sanitizedLocaleCache = new Map();
49
52
  // Insertion order is the eviction order, so reinserting on a hit makes it
@@ -68,30 +71,27 @@ const mapLocales = (transform) => (...locales) => {
68
71
  return [];
69
72
  return locales.filter((locale) => !!locale).map(transform);
70
73
  };
74
+ const sanitizeLocale = (locale) => {
75
+ try {
76
+ const [sanitized] = Intl.Collator.supportedLocalesOf(locale);
77
+ if (sanitized)
78
+ return { locale: sanitized };
79
+ }
80
+ catch { /* reported as non-standard */ }
81
+ return { locale: `${locale}`.toLowerCase(), nonStandard: true };
82
+ };
71
83
  export const sanitizeLocales = mapLocales((locale) => {
72
84
  // Only a string is a faithful key for itself.
73
85
  const cacheable = typeof locale === 'string';
74
- if (cacheable) {
75
- const cached = recallSanitizedLocale(locale);
76
- if (cached !== undefined)
77
- return cached;
78
- }
79
- let current = `${locale}`.toLowerCase();
80
- try {
81
- const [sanitized] = Intl.Collator.supportedLocalesOf(locale);
82
- if (!sanitized)
83
- throw new Error();
84
- current = sanitized;
86
+ let sanitized = cacheable ? recallSanitizedLocale(locale) : undefined;
87
+ if (!sanitized) {
88
+ sanitized = sanitizeLocale(locale);
85
89
  if (cacheable)
86
- rememberSanitizedLocale(locale, current);
90
+ rememberSanitizedLocale(locale, sanitized);
87
91
  }
88
- catch {
89
- // Deliberately not remembered: a locale Intl does not know yet can
90
- // recover, and the warning stays tied to the call rather than to
91
- // whichever logger was installed first.
92
+ if (sanitized.nonStandard)
92
93
  logger.warn(`'${locale}' locale is non-standard.`);
93
- }
94
- return current;
94
+ return sanitized.locale;
95
95
  });
96
96
  // The normalization `config.sanitizeLocales` asks for. A custom transform is
97
97
  // consumer code and every table is keyed by what it returns, so a throwing or
@@ -366,14 +366,17 @@ export const routePrefix = (pathname, routeId) => {
366
366
  const at = [...segments.keys(), segments.length].find((skip) => fits(skip, 0));
367
367
  return at === undefined ? undefined : raw.slice(0, at).map((segment) => `/${segment}`).join('');
368
368
  };
369
+ /** Whether `config.preprocess` is the built-in dot notation. */
370
+ export const dotNotates = (preprocess) => typeof preprocess !== 'function' && preprocess !== 'none';
369
371
  export const toDotNotation = (input, preserveArrays, parentKey) => {
370
372
  if (preserveArrays && Array.isArray(input)) {
371
373
  return input.map((v) => toDotNotation(v, preserveArrays));
372
374
  }
373
375
  if (input && typeof input === 'object') {
374
376
  // Mutated in place (rebuilding per key is quadratic) into a null-prototype
375
- // object, then spread once on the way out — a literal '__proto__' key stays
376
- // an own property instead of reaching the prototype setter.
377
+ // object, then given `Object.prototype` once the last key is written — a
378
+ // spread would copy every key, and a literal '__proto__' key stays an own
379
+ // property either way instead of reaching the prototype setter.
377
380
  const output = Object.create(null);
378
381
  let hasEntries = false;
379
382
  const walk = (node, prefix) => {
@@ -391,7 +394,7 @@ export const toDotNotation = (input, preserveArrays, parentKey) => {
391
394
  };
392
395
  walk(input, parentKey);
393
396
  if (hasEntries) {
394
- return { ...output };
397
+ return Object.setPrototypeOf(output, Object.prototype);
395
398
  }
396
399
  return null;
397
400
  }
@@ -410,14 +413,24 @@ const describeRoute = (route) => {
410
413
  };
411
414
  // The content itself rather than a hash of it: equality stays exact, and the
412
415
  // string repeats what a serialized payload already carries, so it compresses
413
- // with it.
414
- const loaderId = ({ locale, namespace, routes }) => JSON.stringify(routes ? [locale, namespace, routes.map(describeRoute)] : [locale, namespace]);
416
+ // with it. The pairs of one descriptor share its `routes`, so each list is
417
+ // described once per resolution rather than once per pair.
418
+ const loaderId = ({ locale, namespace, routes }, describe) => JSON.stringify(routes ? [locale, namespace, describe(routes)] : [locale, namespace]);
415
419
  // A name shared by two loaders would hand one's records to the other, so
416
420
  // neither keeps it.
417
421
  const withIds = (loaders) => {
422
+ const described = new Map();
423
+ const describe = (routes) => {
424
+ const known = described.get(routes);
425
+ if (known !== undefined)
426
+ return known;
427
+ const description = routes.map(describeRoute);
428
+ described.set(routes, description);
429
+ return description;
430
+ };
418
431
  const ids = loaders.map((loader) => {
419
432
  try {
420
- return loaderId(loader);
433
+ return loaderId(loader, describe);
421
434
  }
422
435
  catch (error) {
423
436
  logError('Cannot derive an id for a loader.', error);
@@ -442,7 +455,8 @@ const withIds = (loaders) => {
442
455
  const reportedDeprecations = new WeakSet();
443
456
  export const resolveLoaders = (input = [], sanitizeLocales = true) => {
444
457
  const sanitize = sanitizerFactory(sanitizeLocales);
445
- return withIds(input.reduce((acc, descriptor) => {
458
+ const resolved = [];
459
+ input.forEach((descriptor) => {
446
460
  try {
447
461
  const { namespace, key, locale, loader, routes, cache } = descriptor;
448
462
  if (key !== undefined && !reportedDeprecations.has(descriptor)) {
@@ -456,24 +470,23 @@ export const resolveLoaders = (input = [], sanitizeLocales = true) => {
456
470
  const locales = unique(sanitize(...asList(locale).filter((name) => name != null)));
457
471
  if (!namespaces.length || !locales.length) {
458
472
  logger.warn('Skipping a loader that names no locale or no namespace.');
459
- return acc;
473
+ return;
460
474
  }
461
- return [
462
- ...acc,
463
- ...locales.flatMap((pairLocale) => namespaces.map((pairNamespace) => ({
475
+ locales.forEach((pairLocale) => namespaces.forEach((pairNamespace) => {
476
+ resolved.push({
464
477
  namespace: pairNamespace,
465
478
  locale: pairLocale,
466
479
  loader,
467
480
  routes,
468
481
  ...(cache === false ? { cache } : {}),
469
- }))),
470
- ];
482
+ });
483
+ }));
471
484
  }
472
485
  catch (error) {
473
486
  logError('Skipping a loader that cannot be read.', error);
474
- return acc;
475
487
  }
476
- }, []));
488
+ });
489
+ return withIds(resolved);
477
490
  };
478
491
  const isMergeable = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
479
492
  // Data reaching a namespace that already holds some — route-scoped chunks of
@@ -487,10 +500,19 @@ export const mergeTranslations = (target, source, path, onConflict) => {
487
500
  onConflict?.(path);
488
501
  return source;
489
502
  }
490
- return Object.keys(source).reduce((acc, key) => ({
491
- ...acc,
492
- [key]: hasOwn(acc, key) ? mergeTranslations(read(acc, key), read(source, key), `${path}.${key}`, onConflict) : read(source, key),
493
- }), target);
503
+ const keys = Object.keys(source);
504
+ if (!keys.length) {
505
+ return target;
506
+ }
507
+ // Mutated in place (rebuilding per key is quadratic) into a null-prototype
508
+ // copy of `target`, then given `Object.prototype` once the last key is
509
+ // written — a literal '__proto__' key stays an own property instead of
510
+ // reaching the prototype setter.
511
+ const output = Object.assign(Object.create(null), target);
512
+ keys.forEach((key) => {
513
+ output[key] = hasOwn(output, key) ? mergeTranslations(read(output, key), read(source, key), `${path}.${key}`, onConflict) : read(source, key);
514
+ });
515
+ return Object.setPrototypeOf(output, Object.prototype);
494
516
  };
495
517
  // What of `target` still shows once `source` is merged over it: the branches
496
518
  // both hold as objects keep what `source` leaves out, and anything else it
@@ -498,12 +520,12 @@ export const mergeTranslations = (target, source, path, onConflict) => {
498
520
  export const maskTranslations = (target, source) => {
499
521
  if (!isMergeable(target) || !isMergeable(source))
500
522
  return undefined;
501
- return Object.keys(target).reduce((acc, key) => {
523
+ return Object.fromEntries(Object.keys(target).flatMap((key) => {
502
524
  if (!hasOwn(source, key))
503
- return { ...acc, [key]: read(target, key) };
525
+ return [[key, read(target, key)]];
504
526
  const masked = maskTranslations(read(target, key), read(source, key));
505
- return masked === undefined ? acc : { ...acc, [key]: masked };
506
- }, {});
527
+ return masked === undefined ? [] : [[key, masked]];
528
+ }));
507
529
  };
508
530
  // What of `target`, dot-notated under `prefix`, still shows once `keys` are
509
531
  // dot-notated beside it: a leaf whose key is among them is gone. A key spelled
@@ -526,7 +548,7 @@ export const maskOutputKeys = (target, keys, prefix, preserveArrays = false) =>
526
548
  if (Array.isArray(target) && entries.every(([key], index) => key === `${index}`))
527
549
  return entries.map(([, value]) => value);
528
550
  }
529
- return entries.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
551
+ return Object.fromEntries(entries);
530
552
  };
531
553
  const isPlainObject = (value) => {
532
554
  if (!value || typeof value !== 'object')
@@ -534,21 +556,27 @@ const isPlainObject = (value) => {
534
556
  const proto = Object.getPrototypeOf(value);
535
557
  return proto === Object.prototype || proto === null;
536
558
  };
537
- // devalue, which SvelteKit serializes load data with, refuses an object with
538
- // an own '__proto__' key. Branches without one are returned as they are.
539
- export const omitProtoKeys = (value) => {
559
+ const holdsProtoKey = (value) => (Array.isArray(value)
560
+ ? value.some(holdsProtoKey)
561
+ : isPlainObject(value) && Object.keys(value).some((key) => key === '__proto__' || holdsProtoKey(value[key])));
562
+ const withoutProtoKeys = (value) => {
540
563
  if (Array.isArray(value)) {
541
- const items = value.map(omitProtoKeys);
542
- return items.some((item, i) => item !== value[i]) ? items : value;
564
+ const items = value.map(withoutProtoKeys);
565
+ return items.some((item, i) => !Object.is(item, value[i])) ? items : value;
543
566
  }
544
567
  if (!isPlainObject(value))
545
568
  return value;
546
569
  const keys = Object.keys(value);
547
- const entries = keys.filter((key) => key !== '__proto__').map((key) => [key, omitProtoKeys(value[key])]);
548
- if (entries.length === keys.length && entries.every(([key, item]) => item === value[key]))
570
+ const entries = keys.filter((key) => key !== '__proto__').map((key) => [key, withoutProtoKeys(value[key])]);
571
+ if (entries.length === keys.length && entries.every(([key, item]) => Object.is(item, value[key])))
549
572
  return value;
550
- return entries.reduce((acc, [key, item]) => ({ ...acc, [key]: item }), {});
573
+ return Object.fromEntries(entries);
551
574
  };
575
+ // devalue, which SvelteKit serializes load data with, refuses an object with
576
+ // an own '__proto__' key. Branches without one are returned as they are. A
577
+ // snapshot runs this over every table, so a scan that copies nothing decides
578
+ // first whether there is anything to leave out.
579
+ export const omitProtoKeys = (value) => (holdsProtoKey(value) ? withoutProtoKeys(value) : value);
552
580
  const reportLoaderConflict = (path) => {
553
581
  logger.warn(`Conflicting translations for '${path}'. Keeping the value of the last loader.`);
554
582
  };
@@ -573,11 +601,12 @@ export const servedLocales = (loaders, tables) => unique([
573
601
  ...Object.keys(tables),
574
602
  ]);
575
603
  /**
576
- * The locales a config serves, sanitized as an instance built from it
577
- * sanitizes them. Resolving the loaders logs what is wrong with them, so a
578
- * caller runs this once per config.
604
+ * The locales a config serves, from its loaders as `resolveLoaders` resolved
605
+ * them, sanitized as an instance built from it sanitizes them. Resolving the
606
+ * loaders logs what is wrong with them, so a caller resolves them once per
607
+ * config.
579
608
  */
580
- export const configLocales = ({ loaders, translations, sanitizeLocales: strategy }) => servedLocales(resolveLoaders(loaders, strategy), translations ? sanitizeTranslationLocales(translations, sanitizerFactory(strategy)) : {});
609
+ export const configLocales = ({ translations, sanitizeLocales: strategy }, loaders) => servedLocales(loaders, translations ? sanitizeTranslationLocales(translations, sanitizerFactory(strategy)) : {});
581
610
  /** A loader as a message names it. `String`, since interpolating a Symbol namespace throws. */
582
611
  export const loaderName = ({ locale, namespace }) => `'${locale}' > '${String(namespace)}'`;
583
612
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltekit-i18n/base",
3
- "version": "3.1.2",
3
+ "version": "3.3.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",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "dev": "svelte-package -i src -o dist -w",
37
- "typecheck": "tsc --noEmit -p tsconfig.json",
37
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p bench",
38
38
  "pretest": "npm run build && npm run typecheck",
39
39
  "test": "vitest run",
40
40
  "pretest:dist": "npm run build",
@@ -42,6 +42,7 @@
42
42
  "build": "svelte-package -i src -o dist",
43
43
  "prepublishOnly": "npm run build",
44
44
  "lint": "eslint --fix .",
45
+ "bench": "node bench/run.ts",
45
46
  "prepare": "simple-git-hooks"
46
47
  },
47
48
  "files": [