@sveltekit-i18n/base 3.2.0 → 3.3.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 +37 -31
- package/dist/I18n.svelte.d.ts +45 -2
- package/dist/I18n.svelte.js +404 -109
- package/dist/kit/define.svelte.js +137 -37
- package/dist/kit/internal.d.ts +8 -0
- package/dist/kit/server.browser.js +1 -1
- package/dist/kit/server.d.ts +1 -1
- package/dist/kit/server.js +17 -2
- package/dist/types.d.ts +38 -6
- package/dist/utils.d.ts +8 -13
- package/dist/utils.js +91 -62
- package/package.json +3 -2
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 = (
|
|
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
|
|
19
|
-
let text = read(
|
|
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
|
-
|
|
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(
|
|
28
|
-
return
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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,
|
|
90
|
+
rememberSanitizedLocale(locale, sanitized);
|
|
87
91
|
}
|
|
88
|
-
|
|
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
|
|
376
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
473
|
+
return;
|
|
460
474
|
}
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
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).
|
|
523
|
+
return Object.fromEntries(Object.keys(target).flatMap((key) => {
|
|
502
524
|
if (!hasOwn(source, key))
|
|
503
|
-
return
|
|
525
|
+
return [[key, read(target, key)]];
|
|
504
526
|
const masked = maskTranslations(read(target, key), read(source, key));
|
|
505
|
-
return masked === undefined ?
|
|
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
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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(
|
|
542
|
-
return items.some((item, i) => item
|
|
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,
|
|
548
|
-
if (entries.length === keys.length && entries.every(([key, item]) => item
|
|
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
|
|
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,
|
|
577
|
-
*
|
|
578
|
-
* caller
|
|
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 = ({
|
|
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.
|
|
3
|
+
"version": "3.3.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",
|
|
@@ -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": [
|