@quazardous/qdadm 2.15.0 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@quazardous/qdcore": "^1.1.0",
55
- "@quazardous/qddebug": "^1.0.0",
55
+ "@quazardous/qddebug": "^1.1.0",
56
56
  "@quazardous/quarkernel": "^2.1.0",
57
57
  "yaml": "^2.8.4"
58
58
  },
package/src/i18n/I18n.ts CHANGED
@@ -70,6 +70,8 @@ export class I18n {
70
70
  // Concurrent domain loads — share one in-flight promise per (locale, domain)
71
71
  // pair across providers and t() callers.
72
72
  private _inflightDomains: Map<string, Promise<void>> = new Map()
73
+ // (locale::key) pairs already announced on the bus — see onMissing.
74
+ private _missingReported: Set<string> = new Set()
73
75
 
74
76
  constructor(options: I18nOptions = {}, deps: I18nDeps = {}) {
75
77
  this._defaultLocale = options.defaultLocale ?? 'en'
@@ -101,7 +103,16 @@ export class I18n {
101
103
  fallbackLocale: this._fallbackLocale,
102
104
  globalAliases: this._currentStrategyAliases(),
103
105
  onMissing: (key, locale) => {
104
- if (this._emitMissing) this._signals?.emit('i18n:missing', { key, locale })
106
+ if (!this._emitMissing) return
107
+ // A missing key is a FACT, not a stream: knowing it once is the whole
108
+ // diagnostic value. Emitting on every resolution turned a signal into
109
+ // a firehose — the same seven nav keys fired 1616 times in two seconds
110
+ // downstream, and each emission was recorded by two collectors, which
111
+ // fed the tick that triggered the next resolution (#1896).
112
+ const seenKey = `${locale}::${key}`
113
+ if (this._missingReported.has(seenKey)) return
114
+ this._missingReported.add(seenKey)
115
+ this._signals?.emit('i18n:missing', { key, locale })
105
116
  },
106
117
  })
107
118
 
@@ -184,6 +195,7 @@ export class I18n {
184
195
  if (this.locale.value === locale && this._loadedLocales.has(locale)) return
185
196
  await this._loadLocale(locale)
186
197
  this.locale.value = locale
198
+ this._resetMissingReports()
187
199
  this._signals?.emit('locale:changed', locale)
188
200
  }
189
201
 
@@ -247,6 +259,17 @@ export class I18n {
247
259
  // Internals
248
260
  // ---------------------------------------------------------------------------
249
261
 
262
+ /**
263
+ * Forget which missing keys were already announced.
264
+ *
265
+ * Called whenever new messages arrive or the locale changes: a key that was
266
+ * missing may now resolve, and one that goes missing again afterwards is
267
+ * news worth reporting once more.
268
+ */
269
+ private _resetMissingReports(): void {
270
+ this._missingReported.clear()
271
+ }
272
+
250
273
  private async _loadLocale(locale: string): Promise<void> {
251
274
  for (const p of this._providers) {
252
275
  const bundle = await p.load(locale)
@@ -300,6 +323,7 @@ export class I18n {
300
323
  this._loadedDomains.add(cacheKey)
301
324
  this._inflightDomains.delete(cacheKey)
302
325
  if (merged) {
326
+ this._resetMissingReports()
303
327
  this._signals?.emit('i18n:domain-loaded', { locale, domain })
304
328
  }
305
329
  })()
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A missing key is announced once, not on every resolution (#1896).
3
+ *
4
+ * Downstream, seven nav labels fired 1616 `i18n:missing` each in two seconds:
5
+ * every render resolved them, every resolution emitted, and each emission was
6
+ * recorded by two collectors that bumped the tick driving the next render.
7
+ * Capping the signal removes the fuel — the diagnostic value of a missing key
8
+ * is knowing it exists, which takes exactly one signal.
9
+ */
10
+ import { describe, it, expect, beforeEach } from 'vitest'
11
+ import { I18n } from '../I18n'
12
+
13
+ function makeI18n(options = {}) {
14
+ const emitted: Array<{ signal: string; payload: unknown }> = []
15
+ const signals = {
16
+ emit: (signal: string, payload?: unknown) => {
17
+ emitted.push({ signal, payload })
18
+ },
19
+ on: () => () => {},
20
+ }
21
+ const i18n = new I18n({ disableDefaultCoreBundle: true, ...options }, { signals })
22
+ const missing = () => emitted.filter((e) => e.signal === 'i18n:missing')
23
+ return { i18n, emitted, missing }
24
+ }
25
+
26
+ describe('I18n — missing keys are announced once', () => {
27
+ it('emits once however many times the key is resolved', () => {
28
+ const { i18n, missing } = makeI18n()
29
+
30
+ for (let i = 0; i < 200; i++) i18n.t('nav.sections.Control')
31
+
32
+ const announced = missing()
33
+ expect(announced).toHaveLength(1)
34
+ expect((announced[0]!.payload as { key: string }).key).toBe('nav.sections.Control')
35
+ })
36
+
37
+ it('still announces each distinct key', () => {
38
+ const { i18n, missing } = makeI18n()
39
+
40
+ for (let i = 0; i < 50; i++) {
41
+ i18n.t('nav.routes.run')
42
+ i18n.t('nav.routes.job')
43
+ i18n.t('breadcrumb.details')
44
+ }
45
+
46
+ expect(missing()).toHaveLength(3)
47
+ expect(missing().map((m) => (m.payload as { key: string }).key).sort()).toEqual([
48
+ 'breadcrumb.details',
49
+ 'nav.routes.job',
50
+ 'nav.routes.run',
51
+ ])
52
+ })
53
+
54
+ it('keeps returning the fallback — capping the signal changes no output', () => {
55
+ const { i18n } = makeI18n()
56
+
57
+ const first = i18n.t('nav.routes.run')
58
+ const later = i18n.t('nav.routes.run')
59
+
60
+ expect(later).toBe(first)
61
+ })
62
+
63
+ it('honours emitMissing: false', () => {
64
+ const { i18n, missing } = makeI18n({ emitMissing: false })
65
+
66
+ i18n.t('nav.routes.run')
67
+
68
+ expect(missing()).toHaveLength(0)
69
+ })
70
+
71
+ it('announces again after the locale changes — the fact may no longer hold', async () => {
72
+ const { i18n, missing } = makeI18n()
73
+
74
+ i18n.t('nav.routes.run')
75
+ expect(missing()).toHaveLength(1)
76
+
77
+ await i18n.changeLocale('fr')
78
+ i18n.t('nav.routes.run')
79
+
80
+ expect(missing()).toHaveLength(2)
81
+ })
82
+ })