@blamejs/core 0.4.13 → 0.4.14
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/CHANGELOG.md +1 -0
- package/lib/i18n.js +98 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- **0.4.13** (2026-04-30) — b.db: streaming query results
|
|
11
12
|
- **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
|
|
12
13
|
- **0.4.11** (2026-04-30) — b.cache: bytes-cap eviction, sliding TTL, tag invalidation
|
|
13
14
|
- **0.4.10** (2026-04-30) — bodyParser multipart: fileFilter + per-field maxBytes/mimeTypes
|
package/lib/i18n.js
CHANGED
|
@@ -385,12 +385,32 @@ function create(opts) {
|
|
|
385
385
|
opts = opts || {};
|
|
386
386
|
validateOpts(opts, [
|
|
387
387
|
"defaultLocale", "locales", "fallbackLocale",
|
|
388
|
-
"translations", "dir",
|
|
389
|
-
"interpolation", "missingKey", "rtlLanguages",
|
|
388
|
+
"translations", "dir", "eagerLocales", "lazyLoad",
|
|
389
|
+
"interpolation", "missingKey", "onMissingKey", "rtlLanguages",
|
|
390
390
|
"observability", "clock",
|
|
391
391
|
], "b.i18n");
|
|
392
392
|
_validateCreateOpts(opts);
|
|
393
393
|
|
|
394
|
+
if (opts.lazyLoad === true && opts.translations) {
|
|
395
|
+
throw _err("BAD_OPT", "i18n.create: lazyLoad: true requires dir-based loading; " +
|
|
396
|
+
"translations: { ... } is inline-only and already complete at create time");
|
|
397
|
+
}
|
|
398
|
+
if (opts.eagerLocales !== undefined) {
|
|
399
|
+
if (!Array.isArray(opts.eagerLocales)) {
|
|
400
|
+
throw _err("BAD_OPT", "i18n.create: eagerLocales must be an array of BCP 47 tags");
|
|
401
|
+
}
|
|
402
|
+
for (var ei = 0; ei < opts.eagerLocales.length; ei++) {
|
|
403
|
+
_validateLocale("i18n.create: eagerLocales[" + ei + "]", opts.eagerLocales[ei]);
|
|
404
|
+
if (opts.locales.indexOf(opts.eagerLocales[ei]) === -1) {
|
|
405
|
+
throw _err("BAD_OPT", "i18n.create: eagerLocales[" + ei + "] '" +
|
|
406
|
+
opts.eagerLocales[ei] + "' must be in locales array");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (opts.onMissingKey !== undefined && typeof opts.onMissingKey !== "function") {
|
|
411
|
+
throw _err("BAD_OPT", "i18n.create: onMissingKey must be a function (key, locale)");
|
|
412
|
+
}
|
|
413
|
+
|
|
394
414
|
var defaultLocale = opts.defaultLocale;
|
|
395
415
|
var locales = opts.locales.slice();
|
|
396
416
|
var fallbackLocale = (opts.fallbackLocale === null) ? null
|
|
@@ -401,22 +421,54 @@ function create(opts) {
|
|
|
401
421
|
var operatorObs = opts.observability || null;
|
|
402
422
|
|
|
403
423
|
// Translations: either inline object or loaded from dir at create.
|
|
424
|
+
// With lazyLoad, only eager locales hit disk now; the rest load on
|
|
425
|
+
// first lookup that resolves to them.
|
|
404
426
|
var translations;
|
|
427
|
+
var lazyLoadEnabled = false;
|
|
428
|
+
var lazyLoadDir = null;
|
|
429
|
+
var loadedSet = new Set();
|
|
405
430
|
if (opts.dir) {
|
|
406
|
-
|
|
431
|
+
if (opts.lazyLoad === true) {
|
|
432
|
+
lazyLoadEnabled = true;
|
|
433
|
+
lazyLoadDir = opts.dir;
|
|
434
|
+
var eager = Array.isArray(opts.eagerLocales) && opts.eagerLocales.length > 0
|
|
435
|
+
? opts.eagerLocales
|
|
436
|
+
: [defaultLocale];
|
|
437
|
+
translations = _loadFromDir(opts.dir, eager);
|
|
438
|
+
for (var ei2 = 0; ei2 < eager.length; ei2++) loadedSet.add(eager[ei2]);
|
|
439
|
+
} else {
|
|
440
|
+
translations = _loadFromDir(opts.dir, locales);
|
|
441
|
+
for (var ei3 = 0; ei3 < locales.length; ei3++) loadedSet.add(locales[ei3]);
|
|
442
|
+
}
|
|
407
443
|
} else if (opts.translations) {
|
|
408
444
|
translations = opts.translations;
|
|
445
|
+
for (var ei4 = 0; ei4 < locales.length; ei4++) {
|
|
446
|
+
if (translations[locales[ei4]]) loadedSet.add(locales[ei4]);
|
|
447
|
+
}
|
|
409
448
|
} else {
|
|
410
449
|
translations = {};
|
|
411
450
|
}
|
|
451
|
+
var onMissingKey = opts.onMissingKey || null;
|
|
412
452
|
// Validate translation trees up-front so plural-shape errors surface
|
|
413
|
-
// at boot, not at the first request that hits the broken key.
|
|
453
|
+
// at boot, not at the first request that hits the broken key. Lazy
|
|
454
|
+
// locales validate on first load.
|
|
414
455
|
for (var li = 0; li < locales.length; li++) {
|
|
415
456
|
var loc = locales[li];
|
|
416
457
|
if (translations[loc]) {
|
|
417
458
|
_validateTranslationTree(loc, translations[loc], "");
|
|
418
459
|
}
|
|
419
460
|
}
|
|
461
|
+
|
|
462
|
+
function _ensureLocaleLoaded(locale) {
|
|
463
|
+
if (loadedSet.has(locale)) return;
|
|
464
|
+
if (!lazyLoadEnabled || !lazyLoadDir) return; // not configured for lazy
|
|
465
|
+
if (!localesSet.has(locale)) return; // unknown locale; lookup falls through
|
|
466
|
+
var loaded = _loadFromDir(lazyLoadDir, [locale]);
|
|
467
|
+
translations[locale] = loaded[locale];
|
|
468
|
+
_validateTranslationTree(locale, translations[locale], "");
|
|
469
|
+
loadedSet.add(locale);
|
|
470
|
+
_emitObs("i18n.lazyLoad", { locale: locale });
|
|
471
|
+
}
|
|
420
472
|
var localesSet = new Set(locales);
|
|
421
473
|
var currentLocale = defaultLocale;
|
|
422
474
|
|
|
@@ -493,6 +545,7 @@ function create(opts) {
|
|
|
493
545
|
var chain = _localeChain(locale);
|
|
494
546
|
for (var i = 0; i < chain.length; i++) {
|
|
495
547
|
var loc = chain[i];
|
|
548
|
+
_ensureLocaleLoaded(loc);
|
|
496
549
|
if (!translations[loc]) continue;
|
|
497
550
|
var v = _resolveKey(translations[loc], key);
|
|
498
551
|
if (v !== undefined) {
|
|
@@ -502,8 +555,20 @@ function create(opts) {
|
|
|
502
555
|
return null;
|
|
503
556
|
}
|
|
504
557
|
|
|
505
|
-
|
|
506
|
-
|
|
558
|
+
// Ordinal-plural rules cache — separate from cardinal because Intl.PluralRules
|
|
559
|
+
// is type-fixed at construction.
|
|
560
|
+
var ordinalRulesByLocale = {};
|
|
561
|
+
function _ordinalRulesFor(locale) {
|
|
562
|
+
var r = ordinalRulesByLocale[locale];
|
|
563
|
+
if (!r) {
|
|
564
|
+
r = new Intl.PluralRules(locale, { type: "ordinal" });
|
|
565
|
+
ordinalRulesByLocale[locale] = r;
|
|
566
|
+
}
|
|
567
|
+
return r;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function _selectPlural(node, count, locale, ordinal) {
|
|
571
|
+
var rules = ordinal ? _ordinalRulesFor(locale) : _pluralRulesFor(locale);
|
|
507
572
|
var category = rules.select(count);
|
|
508
573
|
if (typeof node[category] === "string") return node[category];
|
|
509
574
|
// Fallback within the entry: "other" was validated mandatory at load.
|
|
@@ -520,6 +585,10 @@ function create(opts) {
|
|
|
520
585
|
|
|
521
586
|
if (!found) {
|
|
522
587
|
_emitObs("i18n.missing", { locale: locale, key: key });
|
|
588
|
+
if (onMissingKey) {
|
|
589
|
+
try { onMissingKey(key, locale); }
|
|
590
|
+
catch (_e) { /* hook is best-effort; never break the request */ }
|
|
591
|
+
}
|
|
523
592
|
if (callerOpts.default !== undefined) return callerOpts.default;
|
|
524
593
|
if (typeof missingKeyPolicy === "function") {
|
|
525
594
|
return missingKeyPolicy(key, locale);
|
|
@@ -540,7 +609,7 @@ function create(opts) {
|
|
|
540
609
|
raw = found.value;
|
|
541
610
|
} else if (_isPluralShape(found.value)) {
|
|
542
611
|
var count = (vars && typeof vars.count === "number") ? vars.count : 0;
|
|
543
|
-
raw = _selectPlural(found.value, count, found.foundIn);
|
|
612
|
+
raw = _selectPlural(found.value, count, found.foundIn, callerOpts.ordinal === true);
|
|
544
613
|
} else {
|
|
545
614
|
// Operator stored a nested tree at this key but called t() against
|
|
546
615
|
// the namespace. Return the key-path as a missing-key signal.
|
|
@@ -560,6 +629,20 @@ function create(opts) {
|
|
|
560
629
|
return t(key, merged, callerOpts);
|
|
561
630
|
}
|
|
562
631
|
|
|
632
|
+
// to — ordinal-plural counterpart of tn. Selects from the entry using
|
|
633
|
+
// Intl.PluralRules({ type: "ordinal" }), so English keys
|
|
634
|
+
// { one: "{count}st", two: "{count}nd", few: "{count}rd", other: "{count}th" }
|
|
635
|
+
// resolve as "1st", "2nd", "3rd", "4th", "21st", etc.
|
|
636
|
+
function to(key, count, vars, callerOpts) {
|
|
637
|
+
if (typeof count !== "number" || !isFinite(count)) {
|
|
638
|
+
throw _err("BAD_INPUT", "i18n.to: count must be a finite number, got " +
|
|
639
|
+
(typeof count) + " " + JSON.stringify(count));
|
|
640
|
+
}
|
|
641
|
+
var merged = vars ? Object.assign({}, vars, { count: count }) : { count: count };
|
|
642
|
+
var withOrdinal = Object.assign({}, callerOpts || {}, { ordinal: true });
|
|
643
|
+
return t(key, merged, withOrdinal);
|
|
644
|
+
}
|
|
645
|
+
|
|
563
646
|
function has(key, callerOpts) {
|
|
564
647
|
callerOpts = callerOpts || {};
|
|
565
648
|
if (typeof key !== "string" || key.length === 0) return false;
|
|
@@ -727,6 +810,11 @@ function create(opts) {
|
|
|
727
810
|
if (c.locale === undefined) c.locale = resolvedLocale;
|
|
728
811
|
return tn(key, count, vars, c);
|
|
729
812
|
}
|
|
813
|
+
function reqTo(key, count, vars, callerOpts) {
|
|
814
|
+
var c = callerOpts ? Object.assign({}, callerOpts) : {};
|
|
815
|
+
if (c.locale === undefined) c.locale = resolvedLocale;
|
|
816
|
+
return to(key, count, vars, c);
|
|
817
|
+
}
|
|
730
818
|
function reqDir() {
|
|
731
819
|
return dir({ locale: resolvedLocale });
|
|
732
820
|
}
|
|
@@ -734,12 +822,14 @@ function create(opts) {
|
|
|
734
822
|
req.locale = resolvedLocale;
|
|
735
823
|
req.t = reqT;
|
|
736
824
|
req.tn = reqTn;
|
|
825
|
+
req.to = reqTo;
|
|
737
826
|
req.dir = reqDir;
|
|
738
827
|
if (res && typeof res === "object") {
|
|
739
828
|
if (!res.locals) res.locals = {};
|
|
740
829
|
res.locals.locale = resolvedLocale;
|
|
741
830
|
res.locals.t = reqT;
|
|
742
831
|
res.locals.tn = reqTn;
|
|
832
|
+
res.locals.to = reqTo;
|
|
743
833
|
res.locals.dir = reqDir();
|
|
744
834
|
}
|
|
745
835
|
} catch (_e) {
|
|
@@ -753,6 +843,7 @@ function create(opts) {
|
|
|
753
843
|
return {
|
|
754
844
|
t: t,
|
|
755
845
|
tn: tn,
|
|
846
|
+
to: to,
|
|
756
847
|
has: has,
|
|
757
848
|
formatNumber: formatNumber,
|
|
758
849
|
formatDate: formatDate,
|