@schukai/monster 4.149.1 → 4.150.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.
Files changed (91) hide show
  1. package/package.json +1 -1
  2. package/source/components/accessibility/locale-picker.mjs +22 -14
  3. package/source/components/accessibility/locale-select.mjs +2 -0
  4. package/source/components/content/stylesheet/camera-capture.mjs +1 -1
  5. package/source/components/content/stylesheet/copy.mjs +1 -1
  6. package/source/components/content/viewer/message.mjs +22 -0
  7. package/source/components/content/viewer/stylesheet/message.mjs +1 -1
  8. package/source/components/content/viewer.mjs +6 -6
  9. package/source/components/datatable/datatable.mjs +139 -7
  10. package/source/components/datatable/filter/date-range.mjs +168 -3
  11. package/source/components/datatable/filter/input.mjs +47 -3
  12. package/source/components/datatable/filter/range.mjs +91 -3
  13. package/source/components/datatable/filter-button.mjs +47 -3
  14. package/source/components/datatable/filter.mjs +11 -0
  15. package/source/components/datatable/save-button.mjs +47 -3
  16. package/source/components/datatable/style/column-bar.pcss +4 -1
  17. package/source/components/datatable/style/datatable.pcss +12 -3
  18. package/source/components/datatable/style/filter-controls-defaults.pcss +4 -1
  19. package/source/components/datatable/stylesheet/column-bar.mjs +3 -1
  20. package/source/components/datatable/stylesheet/datatable.mjs +11 -3
  21. package/source/components/datatable/stylesheet/filter-controls-defaults.mjs +5 -1
  22. package/source/components/files/file-manager.mjs +96 -13
  23. package/source/components/form/buy-box.mjs +87 -87
  24. package/source/components/form/cart-control.mjs +24 -24
  25. package/source/components/form/credential-button.mjs +13 -0
  26. package/source/components/form/field-set.mjs +80 -3
  27. package/source/components/form/login.mjs +143 -96
  28. package/source/components/form/register-wizard.mjs +298 -298
  29. package/source/components/form/select.mjs +103 -79
  30. package/source/components/form/style/checklist.pcss +4 -1
  31. package/source/components/form/style/choice-cards.pcss +16 -4
  32. package/source/components/form/style/digits.pcss +4 -1
  33. package/source/components/form/style/field-layout.pcss +4 -1
  34. package/source/components/form/style/mixin/field-set-layout.pcss +8 -2
  35. package/source/components/form/style/select.pcss +12 -3
  36. package/source/components/form/style/toggle-switch.pcss +8 -0
  37. package/source/components/form/style/variant-select.pcss +8 -2
  38. package/source/components/form/stylesheet/button-bar.mjs +1 -1
  39. package/source/components/form/stylesheet/checklist.mjs +3 -1
  40. package/source/components/form/stylesheet/choice-cards.mjs +9 -1
  41. package/source/components/form/stylesheet/confirm-button.mjs +1 -1
  42. package/source/components/form/stylesheet/context-error.mjs +1 -1
  43. package/source/components/form/stylesheet/context-help.mjs +1 -1
  44. package/source/components/form/stylesheet/control-bar.mjs +1 -1
  45. package/source/components/form/stylesheet/digits.mjs +5 -1
  46. package/source/components/form/stylesheet/field-layout.mjs +5 -1
  47. package/source/components/form/stylesheet/field-set.mjs +5 -1
  48. package/source/components/form/stylesheet/form.mjs +3 -1
  49. package/source/components/form/stylesheet/input-group.mjs +3 -1
  50. package/source/components/form/stylesheet/login.mjs +3 -1
  51. package/source/components/form/stylesheet/popper-button.mjs +3 -1
  52. package/source/components/form/stylesheet/register-wizard.mjs +3 -1
  53. package/source/components/form/stylesheet/repeat-field-set-items.mjs +3 -1
  54. package/source/components/form/stylesheet/select.mjs +8 -2
  55. package/source/components/form/stylesheet/toggle-switch.mjs +5 -1
  56. package/source/components/form/stylesheet/variant-select.mjs +5 -1
  57. package/source/components/form/variant-select.mjs +17 -17
  58. package/source/components/form/wizard.mjs +20 -20
  59. package/source/components/host/stylesheet/host.mjs +3 -1
  60. package/source/components/layout/details.mjs +47 -3
  61. package/source/components/layout/style/tabs.pcss +16 -4
  62. package/source/components/layout/style/vertical-tabs.pcss +4 -1
  63. package/source/components/layout/stylesheet/overlay.mjs +3 -1
  64. package/source/components/layout/stylesheet/popper.mjs +1 -1
  65. package/source/components/layout/stylesheet/tabs.mjs +12 -4
  66. package/source/components/layout/stylesheet/vertical-tabs.mjs +3 -1
  67. package/source/components/layout/tabs.mjs +65 -10
  68. package/source/components/layout/vertical-tabs.mjs +65 -10
  69. package/source/components/style/floating-ui.css +13 -0
  70. package/source/components/style/floating-ui.pcss +4 -0
  71. package/source/components/style/form.css +3 -1
  72. package/source/components/style/form.pcss +8 -2
  73. package/source/components/style/theme-modern.css +2 -2
  74. package/source/components/style/theme-modern.pcss +4 -4
  75. package/source/components/stylesheet/floating-ui.mjs +1 -1
  76. package/source/components/stylesheet/form.mjs +3 -1
  77. package/source/components/stylesheet/theme-modern.mjs +2 -2
  78. package/source/components/time/month-calendar.mjs +75 -2
  79. package/source/components/tree-menu/style/tree-menu.pcss +12 -1
  80. package/source/components/tree-menu/stylesheet/tree-menu.mjs +7 -1
  81. package/source/i18n/internal.mjs +60 -18
  82. package/source/i18n/map/languages.mjs +2 -1
  83. package/source/i18n/time-ago.mjs +129 -0
  84. package/test/cases/components/datatable/datasource-rest-lifecycle.mjs +9 -1
  85. package/test/cases/components/form/locale-defaults.mjs +58 -0
  86. package/test/cases/components/form/popper-button.mjs +12 -0
  87. package/test/cases/components/form/register-wizard.mjs +15 -0
  88. package/test/cases/components/style/theme-accent.mjs +137 -0
  89. package/test/cases/components/style/theme-modern.mjs +10 -1
  90. package/test/cases/i18n/built-in-locales.mjs +151 -0
  91. package/test/cases/i18n/time-ago.mjs +52 -32
@@ -439,6 +439,135 @@ const translations = {
439
439
  },
440
440
  },
441
441
 
442
+ bn: {
443
+ template: {
444
+ on: (value) => `${value}-এ`,
445
+ past: (value) => `${value} আগে`,
446
+ future: (value) => `${value} পরে`,
447
+ justNow: () => `এইমাত্র`,
448
+ },
449
+ units: {
450
+ day: {
451
+ zero: "দিন",
452
+ one: "দিন",
453
+ two: "দিন",
454
+ few: "দিন",
455
+ many: "দিন",
456
+ other: "দিন",
457
+ },
458
+ hour: {
459
+ zero: "ঘণ্টা",
460
+ one: "ঘণ্টা",
461
+ two: "ঘণ্টা",
462
+ few: "ঘণ্টা",
463
+ many: "ঘণ্টা",
464
+ other: "ঘণ্টা",
465
+ },
466
+ minute: {
467
+ zero: "মিনিট",
468
+ one: "মিনিট",
469
+ two: "মিনিট",
470
+ few: "মিনিট",
471
+ many: "মিনিট",
472
+ other: "মিনিট",
473
+ },
474
+ second: {
475
+ zero: "সেকেন্ড",
476
+ one: "সেকেন্ড",
477
+ two: "সেকেন্ড",
478
+ few: "সেকেন্ড",
479
+ many: "সেকেন্ড",
480
+ other: "সেকেন্ড",
481
+ },
482
+ },
483
+ },
484
+
485
+ pa: {
486
+ template: {
487
+ on: (value) => `${value} ਨੂੰ`,
488
+ past: (value) => `${value} ਪਹਿਲਾਂ`,
489
+ future: (value) => `${value} ਬਾਅਦ`,
490
+ justNow: () => `ਹੁਣੇ`,
491
+ },
492
+ units: {
493
+ day: {
494
+ zero: "ਦਿਨ",
495
+ one: "ਦਿਨ",
496
+ two: "ਦਿਨ",
497
+ few: "ਦਿਨ",
498
+ many: "ਦਿਨ",
499
+ other: "ਦਿਨ",
500
+ },
501
+ hour: {
502
+ zero: "ਘੰਟੇ",
503
+ one: "ਘੰਟਾ",
504
+ two: "ਘੰਟੇ",
505
+ few: "ਘੰਟੇ",
506
+ many: "ਘੰਟੇ",
507
+ other: "ਘੰਟੇ",
508
+ },
509
+ minute: {
510
+ zero: "ਮਿੰਟ",
511
+ one: "ਮਿੰਟ",
512
+ two: "ਮਿੰਟ",
513
+ few: "ਮਿੰਟ",
514
+ many: "ਮਿੰਟ",
515
+ other: "ਮਿੰਟ",
516
+ },
517
+ second: {
518
+ zero: "ਸਕਿੰਟ",
519
+ one: "ਸਕਿੰਟ",
520
+ two: "ਸਕਿੰਟ",
521
+ few: "ਸਕਿੰਟ",
522
+ many: "ਸਕਿੰਟ",
523
+ other: "ਸਕਿੰਟ",
524
+ },
525
+ },
526
+ },
527
+
528
+ mr: {
529
+ template: {
530
+ on: (value) => `${value} रोजी`,
531
+ past: (value) => `${value} पूर्वी`,
532
+ future: (value) => `${value} नंतर`,
533
+ justNow: () => `आत्ताच`,
534
+ },
535
+ units: {
536
+ day: {
537
+ zero: "दिवस",
538
+ one: "दिवस",
539
+ two: "दिवस",
540
+ few: "दिवस",
541
+ many: "दिवस",
542
+ other: "दिवस",
543
+ },
544
+ hour: {
545
+ zero: "तास",
546
+ one: "तास",
547
+ two: "तास",
548
+ few: "तास",
549
+ many: "तास",
550
+ other: "तास",
551
+ },
552
+ minute: {
553
+ zero: "मिनिटे",
554
+ one: "मिनिट",
555
+ two: "मिनिटे",
556
+ few: "मिनिटे",
557
+ many: "मिनिटे",
558
+ other: "मिनिटे",
559
+ },
560
+ second: {
561
+ zero: "सेकंद",
562
+ one: "सेकंद",
563
+ two: "सेकंद",
564
+ few: "सेकंद",
565
+ many: "सेकंद",
566
+ other: "सेकंद",
567
+ },
568
+ },
569
+ },
570
+
442
571
  pt: {
443
572
  template: {
444
573
  on: (value) => `em ${value}`,
@@ -335,7 +335,7 @@ describe("REST datasource lifecycle", function () {
335
335
  expectNoPendingChanges(second);
336
336
  });
337
337
 
338
- it("preserves a read baseline when the save write finishes later", async function () {
338
+ it("preserves the exact read baseline when the save write finishes later", async function () {
339
339
  const datasource = createDatasource("read-first-datasource");
340
340
  const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
341
341
  const written = { data: [{ id: "area-1", name: "Written change" }] };
@@ -373,6 +373,14 @@ describe("REST datasource lifecycle", function () {
373
373
  writeResult.resolve({ ok: true });
374
374
  await waitForUpdates();
375
375
  expectPendingChanges(button);
376
+
377
+ datasource.data = refreshed;
378
+ await waitForUpdates();
379
+ expectNoPendingChanges(button);
380
+
381
+ datasource.data = written;
382
+ await waitForUpdates();
383
+ expectPendingChanges(button);
376
384
  });
377
385
 
378
386
  it("establishes the read baseline when the save write finishes first", async function () {
@@ -0,0 +1,58 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { getGlobal } from "../../../../source/types/global.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
5
+
6
+ describe("localized form defaults", () => {
7
+ before(async () => {
8
+ await initJSDOM();
9
+ await import("element-internals-polyfill");
10
+
11
+ const global = getGlobal();
12
+ global.ResizeObserver ??= ResizeObserverMock;
13
+
14
+ await Promise.all([
15
+ import("../../../../source/components/form/buy-box.mjs"),
16
+ import("../../../../source/components/form/cart-control.mjs"),
17
+ import("../../../../source/components/form/login.mjs"),
18
+ import("../../../../source/components/form/select.mjs"),
19
+ import("../../../../source/components/form/variant-select.mjs"),
20
+ import("../../../../source/components/form/wizard.mjs"),
21
+ ]);
22
+ });
23
+
24
+ afterEach(() => {
25
+ document.documentElement.lang = "en";
26
+ document.getElementById("mocks").replaceChildren();
27
+ });
28
+
29
+ it("uses native text for localized component options", () => {
30
+ for (const [tag, locale, option, expected] of [
31
+ [
32
+ "monster-buy-box",
33
+ "de",
34
+ "labels.selectVariant",
35
+ "Bitte eine Variante wählen.",
36
+ ],
37
+ ["monster-buy-box", "ru", "labels.addToCart", "В корзину"],
38
+ ["monster-cart-control", "cs", "labels.items", "Položky"],
39
+ ["monster-login", "hi", "labels.username", "उपयोगकर्ता नाम या ई-मेल"],
40
+ ["monster-login", "bn", "labels.password", "পাসওয়ার্ড"],
41
+ ["monster-login", "pa", "labels.login", "ਲਾਗ ਇਨ ਕਰੋ"],
42
+ ["monster-login", "mr", "labels.mailAddress", "ई-मेल पत्ता"],
43
+ ["monster-login", "fi", "labels.login", "Kirjaudu sisään"],
44
+ ["monster-variant-select", "pt", "messages.valid", "A seleção é válida."],
45
+ [
46
+ "monster-select",
47
+ "fr",
48
+ "labels.no-options-available",
49
+ "Aucune option disponible.",
50
+ ],
51
+ ["monster-wizard", "sv", "labels.submit", "Slutför"],
52
+ ]) {
53
+ document.documentElement.lang = locale;
54
+ const element = document.createElement(tag);
55
+ assert.equal(element.getOption(option), expected, `${tag}:${locale}`);
56
+ }
57
+ });
58
+ });
@@ -48,6 +48,18 @@ describe("PopperButton", function () {
48
48
  expect(cssText).to.contain("textarea{resize:vertical;min-height:6rem");
49
49
  });
50
50
 
51
+ it("should preserve focus indicator space inside clipped popper content", function () {
52
+ const cssText = PopperButton.getCSSStyleSheet()
53
+ .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
54
+ .map((rule) => rule.cssText)
55
+ .join("\n")
56
+ .replaceAll(/\s+/g, "");
57
+
58
+ expect(cssText).to.contain(
59
+ "[part=content]:not([data-monster-overflow-mode=visible]){padding:var(--monster-popper-focus-bleed,max(0px,calc(var(--monster-focus-ring-width,2px)+var(--monster-focus-ring-offset,2px))))",
60
+ );
61
+ });
62
+
51
63
  it("should apply content overflow mode to the rendered content wrapper", function (done) {
52
64
  let mocks = document.getElementById("mocks");
53
65
  const button = document.createElement("monster-popper-button");
@@ -18,6 +18,21 @@ describe("RegisterWizard", () => {
18
18
 
19
19
  afterEach(() => {
20
20
  document.getElementById("mocks").replaceChildren();
21
+ document.documentElement.lang = "en";
22
+ });
23
+
24
+ it("uses native localized defaults", () => {
25
+ for (const [locale, option, expected] of [
26
+ ["de", "labels.street", "Straße"],
27
+ ["hi", "labels.successTitle", "पंजीकरण पूरा हुआ"],
28
+ ["ru", "labels.stepAccount", "Учётная запись"],
29
+ ["ja", "labels.successTitle", "登録完了"],
30
+ ["fi", "labels.email", "Sähköposti"],
31
+ ]) {
32
+ document.documentElement.lang = locale;
33
+ const element = document.createElement("monster-register-wizard");
34
+ assert.equal(element.getOption(option), expected, `${locale}:${option}`);
35
+ }
21
36
  });
22
37
 
23
38
  it("keeps explicit labels inside nested field sets", async () => {
@@ -0,0 +1,137 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { expect } from "chai";
4
+
5
+ const projectRoot = resolve(import.meta.dirname, "../../../..");
6
+ const componentRoot = join(projectRoot, "source/components");
7
+
8
+ const sourceFiles = {
9
+ tabs: "layout/style/tabs.pcss",
10
+ verticalTabs: "layout/style/vertical-tabs.pcss",
11
+ choiceCards: "form/style/choice-cards.pcss",
12
+ select: "form/style/select.pcss",
13
+ variantSelect: "form/style/variant-select.pcss",
14
+ datatable: "datatable/style/datatable.pcss",
15
+ treeMenu: "tree-menu/style/tree-menu.pcss",
16
+ themeModern: "style/theme-modern.pcss",
17
+ };
18
+
19
+ const generatedFiles = {
20
+ tabs: "layout/stylesheet/tabs.mjs",
21
+ verticalTabs: "layout/stylesheet/vertical-tabs.mjs",
22
+ choiceCards: "form/stylesheet/choice-cards.mjs",
23
+ select: "form/stylesheet/select.mjs",
24
+ variantSelect: "form/stylesheet/variant-select.mjs",
25
+ datatable: "datatable/stylesheet/datatable.mjs",
26
+ treeMenu: "tree-menu/stylesheet/tree-menu.mjs",
27
+ toggleSwitch: "form/stylesheet/toggle-switch.mjs",
28
+ themeModern: "stylesheet/theme-modern.mjs",
29
+ };
30
+
31
+ describe("Theme accent contract", () => {
32
+ it("gives every native accent-color declaration a legacy fallback", () => {
33
+ const declarations = [];
34
+
35
+ for (const file of walkPcssFiles(componentRoot)) {
36
+ const source = readFileSync(file, "utf8");
37
+ for (const match of source.matchAll(/accent-color:\s*([^;]+);/gu)) {
38
+ declarations.push({
39
+ file: relative(componentRoot, file),
40
+ value: match[1],
41
+ });
42
+ }
43
+ }
44
+
45
+ expect(declarations).to.have.length.greaterThan(0);
46
+ for (const { file, value } of declarations) {
47
+ expect(value, file).to.include("--monster-theme-accent");
48
+ expect(value, file).to.match(
49
+ /var\(\s*--monster-theme-accent,\s*var\(\s*--monster-/u,
50
+ );
51
+ }
52
+ });
53
+
54
+ it("uses the accent pair for active and selected controls", () => {
55
+ const sources = Object.fromEntries(
56
+ Object.entries(sourceFiles).map(([name, file]) => [
57
+ name,
58
+ readFileSync(join(componentRoot, file), "utf8"),
59
+ ]),
60
+ );
61
+
62
+ for (const name of [
63
+ "tabs",
64
+ "verticalTabs",
65
+ "choiceCards",
66
+ "select",
67
+ "variantSelect",
68
+ "datatable",
69
+ "treeMenu",
70
+ ]) {
71
+ expect(sources[name], name).to.include("--monster-theme-accent");
72
+ }
73
+
74
+ for (const name of ["select", "variantSelect", "datatable", "treeMenu"]) {
75
+ expect(sources[name], name).to.include("--monster-theme-accent-subtle");
76
+ }
77
+
78
+ expect(sources.choiceCards).to.include("--monster-theme-on-accent");
79
+ const toggleSwitch = readFileSync(
80
+ join(componentRoot, "form/style/toggle-switch.pcss"),
81
+ "utf8",
82
+ );
83
+ expect(toggleSwitch).to.match(
84
+ /\.switch\[data-monster-state="on"\] \.switch-slider[\s\S]*?--monster-theme-accent[\s\S]*?--monster-theme-on-accent/u,
85
+ );
86
+ expect(sources.themeModern).to.include(
87
+ "--monster-tabs-button-active-color: var(--monster-theme-accent)",
88
+ );
89
+ expect(sources.themeModern).to.include(
90
+ "--monster-tabs-button-active-border-color: var(--monster-theme-accent)",
91
+ );
92
+ expect(sources.themeModern).to.include(
93
+ "--monster-theme-on-bg-color: var(--monster-theme-accent)",
94
+ );
95
+ });
96
+
97
+ it("ships the accent contract in generated component styles", () => {
98
+ for (const [name, file] of Object.entries(generatedFiles)) {
99
+ const generated = readFileSync(join(componentRoot, file), "utf8");
100
+ expect(generated, name).to.include("--monster-theme-accent");
101
+ }
102
+
103
+ const generatedTheme = readFileSync(
104
+ join(componentRoot, generatedFiles.themeModern),
105
+ "utf8",
106
+ );
107
+ expect(generatedTheme).to.include(
108
+ "--monster-tabs-button-active-color:var(--monster-theme-accent)",
109
+ );
110
+ expect(generatedTheme).to.include(
111
+ "--monster-theme-on-bg-color:var(--monster-theme-accent)",
112
+ );
113
+ });
114
+
115
+ it("keeps generated stylesheet imports independent of the checkout path", () => {
116
+ for (const file of walkFiles(componentRoot, ".mjs")) {
117
+ const source = readFileSync(file, "utf8");
118
+ expect(source, relative(componentRoot, file)).not.to.match(
119
+ /from\s+"[^"]*monster\/source\//u,
120
+ );
121
+ }
122
+ });
123
+ });
124
+
125
+ function walkPcssFiles(directory) {
126
+ return walkFiles(directory, ".pcss");
127
+ }
128
+
129
+ function walkFiles(directory, extension) {
130
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
131
+ const path = join(directory, entry.name);
132
+ if (entry.isDirectory()) {
133
+ return walkFiles(path, extension);
134
+ }
135
+ return entry.isFile() && entry.name.endsWith(extension) ? [path] : [];
136
+ });
137
+ }
@@ -85,6 +85,9 @@ const requiredCompatibilityTokens = [
85
85
  ];
86
86
 
87
87
  const contrastPairs = [
88
+ ["theme-accent", "theme-surface-canvas", 4.5],
89
+ ["theme-accent", "theme-surface", 4.5],
90
+ ["theme-accent", "theme-accent-subtle", 4.5],
88
91
  ["theme-text", "theme-surface-canvas", 4.5],
89
92
  ["theme-text", "theme-surface", 4.5],
90
93
  ["theme-text-secondary", "theme-surface", 4.5],
@@ -227,7 +230,13 @@ describe("Modern theme stylesheet", () => {
227
230
  "--monster-split-panel-handle-border-radius:999px",
228
231
  );
229
232
  expect(generatedCss).to.include(
230
- "--monster-tabs-button-active-border-color:var(--monster-theme-tertiary)",
233
+ "--monster-tabs-button-active-border-color:var(--monster-theme-accent)",
234
+ );
235
+ expect(generatedCss).to.include(
236
+ "--monster-tabs-button-active-color:var(--monster-theme-accent)",
237
+ );
238
+ expect(generatedCss).to.include(
239
+ "--monster-theme-on-bg-color:var(--monster-theme-accent)",
231
240
  );
232
241
  expect(generatedCss).to.include(
233
242
  "--monster-tabs-indicator-active-opacity:0",
@@ -0,0 +1,151 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect } from "chai";
5
+ import * as espree from "espree";
6
+ import { globSync } from "glob";
7
+ import { currentSupportedLanguages } from "../../../source/i18n/map/languages.mjs";
8
+
9
+ const repositoryRoot = resolve(
10
+ dirname(fileURLToPath(import.meta.url)),
11
+ "../../..",
12
+ );
13
+ const sourceFiles = globSync("source/**/*.mjs", {
14
+ cwd: repositoryRoot,
15
+ absolute: true,
16
+ });
17
+
18
+ function walk(node, callback) {
19
+ if (!node || typeof node !== "object") {
20
+ return;
21
+ }
22
+
23
+ if (typeof node.type === "string") {
24
+ callback(node);
25
+ }
26
+
27
+ for (const [key, value] of Object.entries(node)) {
28
+ if (["comments", "loc", "range", "tokens"].includes(key)) {
29
+ continue;
30
+ }
31
+ if (Array.isArray(value)) {
32
+ for (const child of value) {
33
+ walk(child, callback);
34
+ }
35
+ } else {
36
+ walk(value, callback);
37
+ }
38
+ }
39
+ }
40
+
41
+ function getPropertyKey(property) {
42
+ if (property.computed || property.type !== "Property") {
43
+ return null;
44
+ }
45
+ return property.key.name ?? property.key.value ?? null;
46
+ }
47
+
48
+ function inspectLocaleTables() {
49
+ const localeSwitches = [];
50
+ const localeObjects = [];
51
+
52
+ for (const filename of sourceFiles) {
53
+ const source = readFileSync(filename, "utf8");
54
+ const ast = espree.parse(source, {
55
+ ecmaVersion: "latest",
56
+ loc: true,
57
+ range: true,
58
+ sourceType: "module",
59
+ });
60
+
61
+ walk(ast, (node) => {
62
+ if (node.type === "SwitchStatement") {
63
+ const discriminant = source.slice(
64
+ node.discriminant.start,
65
+ node.discriminant.end,
66
+ );
67
+ if (!/\.language\b/.test(discriminant)) {
68
+ return;
69
+ }
70
+
71
+ localeSwitches.push({
72
+ filename,
73
+ line: node.loc.start.line,
74
+ codes: node.cases
75
+ .map((entry) => entry.test?.value)
76
+ .filter((entry) => typeof entry === "string"),
77
+ hasDefault: node.cases.some((entry) => entry.test === null),
78
+ });
79
+ }
80
+
81
+ if (node.type === "ObjectExpression") {
82
+ const keys = node.properties
83
+ .map((property) => getPropertyKey(property))
84
+ .filter((key) => typeof key === "string");
85
+ const supportedKeys = keys.filter((key) =>
86
+ currentSupportedLanguages.includes(key),
87
+ );
88
+ if (supportedKeys.length >= 5) {
89
+ localeObjects.push({
90
+ filename,
91
+ line: node.loc.start.line,
92
+ keys,
93
+ });
94
+ }
95
+ }
96
+ });
97
+ }
98
+
99
+ return { localeSwitches, localeObjects };
100
+ }
101
+
102
+ describe("built-in locale coverage", () => {
103
+ const { localeSwitches, localeObjects } = inspectLocaleTables();
104
+
105
+ it("keeps the supported locale list unique", () => {
106
+ expect(new Set(currentSupportedLanguages).size).to.equal(
107
+ currentSupportedLanguages.length,
108
+ );
109
+ });
110
+
111
+ it("covers every supported locale in language switches", () => {
112
+ expect(localeSwitches.length).to.be.greaterThan(30);
113
+
114
+ for (const table of localeSwitches) {
115
+ const location = `${table.filename}:${table.line}`;
116
+ for (const language of currentSupportedLanguages) {
117
+ if (language === "en" && table.hasDefault) {
118
+ continue;
119
+ }
120
+ expect(table.codes, `${location} is missing ${language}`).to.include(
121
+ language,
122
+ );
123
+ }
124
+ expect(
125
+ table.codes,
126
+ `${location} uses an obsolete Spanish code`,
127
+ ).not.to.include("sp");
128
+ expect(
129
+ table.codes,
130
+ `${location} uses an obsolete Danish code`,
131
+ ).not.to.include("dk");
132
+ expect(
133
+ table.codes,
134
+ `${location} uses an obsolete Swedish code`,
135
+ ).not.to.include("sw");
136
+ }
137
+ });
138
+
139
+ it("covers every supported locale in language maps", () => {
140
+ expect(localeObjects.length).to.be.greaterThan(3);
141
+
142
+ for (const table of localeObjects) {
143
+ const location = `${table.filename}:${table.line}`;
144
+ for (const language of currentSupportedLanguages) {
145
+ expect(table.keys, `${location} is missing ${language}`).to.include(
146
+ language,
147
+ );
148
+ }
149
+ }
150
+ });
151
+ });
@@ -1,33 +1,53 @@
1
-
2
-
3
- import {expect} from "chai"
4
- import {formatTimeAgo} from "../../../source/i18n/time-ago.mjs";
5
-
6
- describe('formatTimeAgo', () => {
7
- it('returns "just now" for times less than a second ago', () => {
8
- const now = new Date();
9
- expect(formatTimeAgo(now, 'en')).to.equal('just now');
10
- });
11
-
12
- it('returns "in 1 second" for one second in the future', () => {
13
- const oneSecondFuture = new Date(Date.now() + 1000);
14
- expect(formatTimeAgo(oneSecondFuture, 'en')).to.equal('in 1 second');
15
- });
16
-
17
- it('returns "1 second ago" for one second in the past', () => {
18
- const oneSecondPast = new Date(Date.now() - 1000);
19
- expect(formatTimeAgo(oneSecondPast, 'en')).to.equal('1 second ago');
20
- });
21
-
22
- it('returns "in 1 minute, 30 seconds" for 90 seconds in the future', () => {
23
- const ninetySecondsFuture = new Date(Date.now() + 90000);
24
- expect(formatTimeAgo(ninetySecondsFuture, 'en')).to.equal('in 1 minute and 30 seconds');
25
- });
26
-
27
- it('returns "1 minute, 30 seconds ago" for 90 seconds in the past', () => {
28
- const ninetySecondsPast = new Date(Date.now() - 90000);
29
- expect(formatTimeAgo(ninetySecondsPast, 'en')).to.equal('1 minute and 30 seconds ago');
30
- });
31
-
32
- // Weitere Tests können hinzugefügt werden
1
+ import { expect } from "chai";
2
+ import { currentSupportedLanguages } from "../../../source/i18n/map/languages.mjs";
3
+ import { formatTimeAgo } from "../../../source/i18n/time-ago.mjs";
4
+
5
+ describe("formatTimeAgo", () => {
6
+ it('returns "just now" for times less than a second ago', () => {
7
+ const now = new Date();
8
+ expect(formatTimeAgo(now, "en")).to.equal("just now");
9
+ });
10
+
11
+ it('returns "in 1 second" for one second in the future', () => {
12
+ const oneSecondFuture = new Date(Date.now() + 1000);
13
+ expect(formatTimeAgo(oneSecondFuture, "en")).to.equal("in 1 second");
14
+ });
15
+
16
+ it('returns "1 second ago" for one second in the past', () => {
17
+ const oneSecondPast = new Date(Date.now() - 1000);
18
+ expect(formatTimeAgo(oneSecondPast, "en")).to.equal("1 second ago");
19
+ });
20
+
21
+ it('returns "in 1 minute, 30 seconds" for 90 seconds in the future', () => {
22
+ const ninetySecondsFuture = new Date(Date.now() + 90000);
23
+ expect(formatTimeAgo(ninetySecondsFuture, "en")).to.equal(
24
+ "in 1 minute and 30 seconds",
25
+ );
26
+ });
27
+
28
+ it('returns "1 minute, 30 seconds ago" for 90 seconds in the past', () => {
29
+ const ninetySecondsPast = new Date(Date.now() - 90000);
30
+ expect(formatTimeAgo(ninetySecondsPast, "en")).to.equal(
31
+ "1 minute and 30 seconds ago",
32
+ );
33
+ });
34
+
35
+ it("formats relative times for every supported South Asian locale", () => {
36
+ const oneHourFuture = new Date(Date.now() + 3600000);
37
+
38
+ expect(formatTimeAgo(oneHourFuture, "bn")).to.equal("1 ঘণ্টা পরে");
39
+ expect(formatTimeAgo(oneHourFuture, "pa")).to.equal("1 ਘੰਟਾ ਬਾਅਦ");
40
+ expect(formatTimeAgo(oneHourFuture, "mr")).to.equal("1 तास नंतर");
41
+ });
42
+
43
+ it("formats every supported locale without falling back to a missing table", () => {
44
+ const oneHourFuture = new Date(Date.now() + 3600000);
45
+
46
+ for (const locale of currentSupportedLanguages) {
47
+ expect(formatTimeAgo(oneHourFuture, locale), locale).to.be.a("string").and
48
+ .not.be.empty;
49
+ }
50
+ });
51
+
52
+ // Weitere Tests können hinzugefügt werden
33
53
  });