@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.
|
|
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.
|
|
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)
|
|
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
|
+
})
|