qdadm 1.13.0 → 1.19.2

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 (86) hide show
  1. package/package.json +8 -4
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. package/src/utils/debugInjector.ts +0 -306
@@ -0,0 +1,738 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * I18nPanel - Debug panel for the i18n subsystem.
4
+ *
5
+ * Sections:
6
+ * - Header: active locale, available locales, strategy, providers
7
+ * - Resolve: live resolve() against any key with full trace
8
+ * - Missing: keys captured via i18n:missing (deduped by key+locale, with counts)
9
+ * - Coverage: entity × locale matrix with per-cell drill-down
10
+ * - Bundle: dump of the merged inline bundle for a chosen locale
11
+ */
12
+ import { ref, computed, inject, watch, onMounted } from 'vue'
13
+ import { ObjectTree } from '@quazardous/qddebug'
14
+ import type { I18n } from '../../../../i18n/I18n'
15
+ import type { ResolveTrace } from '../../../../i18n/types'
16
+ import type { I18nCollector, MissingKeyEntry } from '../../I18nCollector'
17
+ import type { Orchestrator } from '../../../../orchestrator/Orchestrator'
18
+
19
+ interface SignalsBus {
20
+ emit: (signal: string, data?: unknown) => void | Promise<void>
21
+ }
22
+
23
+ const props = defineProps<{
24
+ collector: I18nCollector
25
+ entries: MissingKeyEntry[]
26
+ }>()
27
+
28
+ const i18n = inject<I18n | null>('qdadmI18n', null)
29
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
30
+ const signals = inject<SignalsBus | null>('qdadmSignals', null)
31
+
32
+ type Section = 'header' | 'resolve' | 'missing' | 'coverage' | 'bundle'
33
+ const activeSection = ref<Section>('missing')
34
+
35
+ // ─── Header ───────────────────────────────────────────────────────────────
36
+ const currentLocale = computed<string>(() => i18n?.locale.value ?? 'en')
37
+ const availableLocales = ref<string[]>([])
38
+
39
+ async function refreshAvailableLocales() {
40
+ if (!i18n) {
41
+ availableLocales.value = []
42
+ return
43
+ }
44
+ availableLocales.value = await i18n.availableLocales()
45
+ // Always include the active locale even if no provider declares it
46
+ if (!availableLocales.value.includes(currentLocale.value)) {
47
+ availableLocales.value = [currentLocale.value, ...availableLocales.value]
48
+ }
49
+ }
50
+ onMounted(refreshAvailableLocales)
51
+ watch(currentLocale, refreshAvailableLocales)
52
+
53
+ function switchLocale(locale: string) {
54
+ if (signals) {
55
+ void signals.emit('locale:change', locale)
56
+ } else if (i18n) {
57
+ void i18n.changeLocale(locale)
58
+ }
59
+ }
60
+
61
+ // ─── Resolve tester ───────────────────────────────────────────────────────
62
+ const resolveKey = ref('')
63
+ const resolveLocale = ref<string>('')
64
+ watch(currentLocale, (loc) => {
65
+ if (!resolveLocale.value) resolveLocale.value = loc
66
+ }, { immediate: true })
67
+
68
+ const resolveTrace = computed<ResolveTrace | null>(() => {
69
+ if (!i18n || !resolveKey.value) return null
70
+ // Temporarily resolve against the chosen locale (without switching globally)
71
+ // by re-calling resolve via a small helper — kernel.i18n.resolve uses the
72
+ // active locale, so we mutate the instance's locale ref briefly only for
73
+ // resolve(), restore it. This is read-only though; safer to just call
74
+ // resolve which uses locale.value, so we surface the trace for the active
75
+ // locale and let the user switch via the dropdown.
76
+ return i18n.resolve(resolveKey.value)
77
+ })
78
+
79
+ function copyResolveSnippet() {
80
+ if (!resolveTrace.value) return
81
+ const segs = resolveKey.value.split('.')
82
+ const skeleton = buildNested(segs, '…')
83
+ const snippet = `ctx.messages('${resolveLocale.value || currentLocale.value}', ${JSON.stringify(skeleton, null, 2)})`
84
+ void navigator.clipboard?.writeText(snippet)
85
+ }
86
+
87
+ function buildNested(segments: string[], leaf: unknown): Record<string, unknown> {
88
+ const root: Record<string, unknown> = {}
89
+ let cursor: Record<string, unknown> = root
90
+ segments.forEach((seg, i) => {
91
+ if (i === segments.length - 1) {
92
+ cursor[seg] = leaf
93
+ } else {
94
+ cursor[seg] = {}
95
+ cursor = cursor[seg] as Record<string, unknown>
96
+ }
97
+ })
98
+ return root
99
+ }
100
+
101
+ // ─── Missing keys ─────────────────────────────────────────────────────────
102
+ const missingFilter = ref('')
103
+ const missingGrouped = computed<Array<{ ns: string; entries: MissingKeyEntry[] }>>(() => {
104
+ const filter = missingFilter.value.trim().toLowerCase()
105
+ const filtered = filter
106
+ ? props.entries.filter(
107
+ (e) => e.key.toLowerCase().includes(filter) || e.locale.includes(filter)
108
+ )
109
+ : props.entries
110
+ const groups: Record<string, MissingKeyEntry[]> = {}
111
+ for (const e of filtered) {
112
+ const ns = e.key.split('.')[0] ?? '_other'
113
+ if (!groups[ns]) groups[ns] = []
114
+ groups[ns]!.push(e)
115
+ }
116
+ return Object.entries(groups)
117
+ .map(([ns, entries]) => ({ ns, entries: entries.sort((a, b) => a.key.localeCompare(b.key)) }))
118
+ .sort((a, b) => a.ns.localeCompare(b.ns))
119
+ })
120
+
121
+ function copyMissingSkeleton() {
122
+ const skeleton = props.collector.asJsonSkeleton()
123
+ void navigator.clipboard?.writeText(JSON.stringify(skeleton, null, 2))
124
+ }
125
+
126
+ // ─── Coverage matrix ──────────────────────────────────────────────────────
127
+ interface CoverageCell {
128
+ entity: string
129
+ locale: string
130
+ total: number
131
+ hits: number
132
+ status: 'green' | 'yellow' | 'red' | 'none'
133
+ }
134
+
135
+ interface CoverageDrill {
136
+ entity: string
137
+ locale: string
138
+ fields: Array<{ name: string; key: string; hit: boolean; result: string }>
139
+ }
140
+
141
+ const coverageEntities = ref<string[]>([])
142
+ const drillCell = ref<CoverageDrill | null>(null)
143
+
144
+ function refreshCoverageEntities() {
145
+ if (!orchestrator) {
146
+ coverageEntities.value = []
147
+ return
148
+ }
149
+ coverageEntities.value = orchestrator.getRegisteredNames?.() ?? []
150
+ }
151
+ onMounted(refreshCoverageEntities)
152
+
153
+ function getEntityFields(entity: string): string[] {
154
+ const manager = orchestrator?.get?.(entity) as { fields?: Record<string, unknown> } | null
155
+ return manager?.fields ? Object.keys(manager.fields) : []
156
+ }
157
+
158
+ const coverageMatrix = computed<CoverageCell[]>(() => {
159
+ if (!i18n) return []
160
+ // Re-read tick for reactivity (the I18n locale ref is the natural trigger)
161
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
162
+ currentLocale.value
163
+ const cells: CoverageCell[] = []
164
+ for (const entity of coverageEntities.value) {
165
+ const fields = getEntityFields(entity)
166
+ if (fields.length === 0) continue
167
+ for (const locale of availableLocales.value) {
168
+ const prevLocale = i18n.locale.value
169
+ // Briefly point the resolver at the target locale to compute coverage,
170
+ // then restore. We don't emit locale:changed so nothing else reacts.
171
+ i18n.locale.value = locale
172
+ let hits = 0
173
+ for (const f of fields) {
174
+ const trace = i18n.resolve(`entities.${entity}.fields.${f}`)
175
+ if (trace.hit) hits++
176
+ }
177
+ i18n.locale.value = prevLocale
178
+ const total = fields.length
179
+ const ratio = total === 0 ? 0 : hits / total
180
+ let status: CoverageCell['status'] = 'red'
181
+ if (ratio === 1) status = 'green'
182
+ else if (ratio >= 0.5) status = 'yellow'
183
+ else if (hits > 0) status = 'red'
184
+ else status = 'red'
185
+ cells.push({ entity, locale, total, hits, status })
186
+ }
187
+ }
188
+ return cells
189
+ })
190
+
191
+ const coverageByEntity = computed<Record<string, Record<string, CoverageCell>>>(() => {
192
+ const out: Record<string, Record<string, CoverageCell>> = {}
193
+ for (const cell of coverageMatrix.value) {
194
+ if (!out[cell.entity]) out[cell.entity] = {}
195
+ out[cell.entity]![cell.locale] = cell
196
+ }
197
+ return out
198
+ })
199
+
200
+ function drillDown(entity: string, locale: string) {
201
+ if (!i18n) return
202
+ const fields = getEntityFields(entity)
203
+ const prevLocale = i18n.locale.value
204
+ i18n.locale.value = locale
205
+ const detail = fields.map((name) => {
206
+ const key = `entities.${entity}.fields.${name}`
207
+ const trace = i18n.resolve(key)
208
+ return { name, key, hit: trace.hit, result: trace.result }
209
+ })
210
+ i18n.locale.value = prevLocale
211
+ drillCell.value = { entity, locale, fields: detail }
212
+ }
213
+
214
+ // ─── Bundle inspector ─────────────────────────────────────────────────────
215
+ const bundleLocale = ref<string>('')
216
+ watch(currentLocale, (loc) => {
217
+ if (!bundleLocale.value) bundleLocale.value = loc
218
+ }, { immediate: true })
219
+
220
+ const bundleData = computed<Record<string, unknown> | null>(() => {
221
+ if (!i18n || !bundleLocale.value) return null
222
+ return i18n.dump(bundleLocale.value) as Record<string, unknown>
223
+ })
224
+
225
+ function downloadBundle() {
226
+ if (!bundleData.value) return
227
+ const blob = new Blob([JSON.stringify(bundleData.value, null, 2)], { type: 'application/json' })
228
+ const url = URL.createObjectURL(blob)
229
+ const a = document.createElement('a')
230
+ a.href = url
231
+ a.download = `qdadm-i18n-${bundleLocale.value}.json`
232
+ a.click()
233
+ URL.revokeObjectURL(url)
234
+ }
235
+
236
+ // ─── Helpers ──────────────────────────────────────────────────────────────
237
+ function formatTime(ts: number): string {
238
+ return new Date(ts).toLocaleTimeString('en-US', { hour12: false })
239
+ }
240
+ </script>
241
+
242
+ <template>
243
+ <div class="i18n-panel">
244
+ <!-- Section tabs -->
245
+ <div class="i18n-tabs">
246
+ <button
247
+ v-for="s in (['header','resolve','missing','coverage','bundle'] as Section[])"
248
+ :key="s"
249
+ class="i18n-tab"
250
+ :class="{ 'i18n-tab-active': activeSection === s }"
251
+ @click="activeSection = s"
252
+ >
253
+ {{ ({ header: 'State', resolve: 'Resolve', missing: 'Missing', coverage: 'Coverage', bundle: 'Bundle' })[s] }}
254
+ <span v-if="s === 'missing' && entries.length" class="i18n-tab-count">{{ entries.length }}</span>
255
+ </button>
256
+ </div>
257
+
258
+ <div v-if="!i18n" class="i18n-empty">
259
+ <i class="pi pi-info-circle" />
260
+ <span>No i18n subsystem on this kernel.</span>
261
+ </div>
262
+
263
+ <!-- ─── State ────────────────────────────────────────────────── -->
264
+ <div v-else-if="activeSection === 'header'" class="i18n-section">
265
+ <div class="i18n-row">
266
+ <span class="i18n-row-label">Active locale</span>
267
+ <select :value="currentLocale" class="i18n-select" @change="switchLocale(($event.target as HTMLSelectElement).value)">
268
+ <option v-for="loc in availableLocales" :key="loc" :value="loc">{{ loc }}</option>
269
+ </select>
270
+ </div>
271
+ <div class="i18n-row">
272
+ <span class="i18n-row-label">Available</span>
273
+ <span class="i18n-row-value">{{ availableLocales.join(', ') || '—' }}</span>
274
+ </div>
275
+ <div class="i18n-row">
276
+ <span class="i18n-row-label">Locale switches</span>
277
+ <span class="i18n-row-value">
278
+ <span v-for="(c, idx) in collector.getLocaleHistory()" :key="idx" class="i18n-chip">
279
+ {{ c.locale }} <small>{{ formatTime(c.at) }}</small>
280
+ </span>
281
+ <span v-if="collector.getLocaleHistory().length === 0" class="i18n-muted">none yet</span>
282
+ </span>
283
+ </div>
284
+ </div>
285
+
286
+ <!-- ─── Resolve tester ─────────────────────────────────────────── -->
287
+ <div v-else-if="activeSection === 'resolve'" class="i18n-section">
288
+ <div class="i18n-resolve-input">
289
+ <input
290
+ v-model="resolveKey"
291
+ type="text"
292
+ placeholder="entities.books.fields.title"
293
+ class="i18n-key-input"
294
+ />
295
+ <button v-if="resolveTrace" class="debug-toolbar-btn" @click="copyResolveSnippet">
296
+ <i class="pi pi-copy" /> Snippet
297
+ </button>
298
+ </div>
299
+ <div v-if="resolveTrace" class="i18n-resolve-result">
300
+ <div class="i18n-resolve-summary">
301
+ <span class="i18n-resolve-value">"{{ resolveTrace.result }}"</span>
302
+ <span class="i18n-chip" :class="{ 'i18n-chip-hit': resolveTrace.hit, 'i18n-chip-miss': !resolveTrace.hit }">
303
+ {{ resolveTrace.hit ? 'hit' : 'miss' }}
304
+ </span>
305
+ <span class="i18n-muted">in {{ resolveTrace.locale }}</span>
306
+ </div>
307
+ <ol class="i18n-trace">
308
+ <li v-for="(step, idx) in resolveTrace.steps" :key="idx" class="i18n-trace-step" :class="`i18n-step-${step.kind}`">
309
+ <span class="i18n-trace-kind">{{ step.kind }}</span>
310
+ <code class="i18n-trace-detail">{{ JSON.stringify(step) }}</code>
311
+ </li>
312
+ </ol>
313
+ </div>
314
+ <div v-else-if="resolveKey" class="i18n-muted">Type a key above…</div>
315
+ <div v-else class="i18n-muted">Enter a convention key (e.g. <code>entities.books.fields.title</code>) to see how it resolves.</div>
316
+ </div>
317
+
318
+ <!-- ─── Missing keys ────────────────────────────────────────────── -->
319
+ <div v-else-if="activeSection === 'missing'" class="i18n-section">
320
+ <div class="i18n-missing-toolbar">
321
+ <input
322
+ v-model="missingFilter"
323
+ type="text"
324
+ placeholder="filter…"
325
+ class="i18n-key-input"
326
+ />
327
+ <button class="debug-toolbar-btn" :disabled="entries.length === 0" @click="copyMissingSkeleton">
328
+ <i class="pi pi-copy" /> Copy skeleton
329
+ </button>
330
+ <button class="debug-toolbar-btn" :disabled="entries.length === 0" @click="collector.clear()">
331
+ <i class="pi pi-trash" /> Clear
332
+ </button>
333
+ </div>
334
+ <div v-if="entries.length === 0" class="i18n-empty">
335
+ <i class="pi pi-check-circle" />
336
+ <span>No missing keys captured (yet).</span>
337
+ </div>
338
+ <div v-else>
339
+ <div v-for="group in missingGrouped" :key="group.ns" class="i18n-missing-group">
340
+ <div class="i18n-missing-group-header">
341
+ <i class="pi pi-folder" />
342
+ <span class="i18n-missing-group-name">{{ group.ns }}</span>
343
+ <span class="i18n-muted">({{ group.entries.length }})</span>
344
+ </div>
345
+ <div v-for="e in group.entries" :key="`${e.key}|${e.locale}`" class="i18n-missing-entry">
346
+ <code class="i18n-missing-key">{{ e.key }}</code>
347
+ <span class="i18n-chip">{{ e.locale }}</span>
348
+ <span class="i18n-muted">× {{ e.count }}</span>
349
+ <span class="i18n-muted i18n-missing-time">{{ formatTime(e.lastSeen) }}</span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- ─── Coverage matrix ─────────────────────────────────────────── -->
356
+ <div v-else-if="activeSection === 'coverage'" class="i18n-section">
357
+ <div v-if="coverageEntities.length === 0" class="i18n-muted">No registered entities.</div>
358
+ <div v-else>
359
+ <table class="i18n-coverage-table">
360
+ <thead>
361
+ <tr>
362
+ <th>entity</th>
363
+ <th v-for="loc in availableLocales" :key="loc">{{ loc }}</th>
364
+ </tr>
365
+ </thead>
366
+ <tbody>
367
+ <tr v-for="entity in coverageEntities" :key="entity">
368
+ <td class="i18n-coverage-entity">{{ entity }}</td>
369
+ <td v-for="loc in availableLocales" :key="loc"
370
+ class="i18n-coverage-cell"
371
+ :class="`i18n-cov-${coverageByEntity[entity]?.[loc]?.status ?? 'none'}`"
372
+ @click="drillDown(entity, loc)">
373
+ <span v-if="coverageByEntity[entity]?.[loc]">
374
+ {{ coverageByEntity[entity][loc].hits }}/{{ coverageByEntity[entity][loc].total }}
375
+ </span>
376
+ <span v-else>—</span>
377
+ </td>
378
+ </tr>
379
+ </tbody>
380
+ </table>
381
+ <div v-if="drillCell" class="i18n-coverage-drill">
382
+ <div class="i18n-coverage-drill-header">
383
+ <strong>{{ drillCell.entity }}</strong>
384
+ <span class="i18n-chip">{{ drillCell.locale }}</span>
385
+ <button class="debug-toolbar-btn" @click="drillCell = null">
386
+ <i class="pi pi-times" />
387
+ </button>
388
+ </div>
389
+ <ul class="i18n-coverage-drill-list">
390
+ <li v-for="f in drillCell.fields" :key="f.name" :class="{ 'i18n-cov-hit': f.hit, 'i18n-cov-miss': !f.hit }">
391
+ <i :class="['pi', f.hit ? 'pi-check' : 'pi-times']" />
392
+ <code>{{ f.name }}</code>
393
+ <span class="i18n-coverage-result">"{{ f.result }}"</span>
394
+ </li>
395
+ </ul>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <!-- ─── Bundle inspector ────────────────────────────────────────── -->
401
+ <div v-else-if="activeSection === 'bundle'" class="i18n-section">
402
+ <div class="i18n-bundle-toolbar">
403
+ <select v-model="bundleLocale" class="i18n-select">
404
+ <option v-for="loc in availableLocales" :key="loc" :value="loc">{{ loc }}</option>
405
+ </select>
406
+ <button class="debug-toolbar-btn" :disabled="!bundleData" @click="downloadBundle">
407
+ <i class="pi pi-download" /> Dump JSON
408
+ </button>
409
+ </div>
410
+ <div v-if="bundleData" class="i18n-bundle-tree">
411
+ <ObjectTree :data="bundleData" :expanded="false" />
412
+ </div>
413
+ <div v-else class="i18n-muted">No bundle loaded for this locale.</div>
414
+ </div>
415
+ </div>
416
+ </template>
417
+
418
+ <style scoped>
419
+ .i18n-panel {
420
+ display: flex;
421
+ flex-direction: column;
422
+ height: 100%;
423
+ font-size: 12px;
424
+ }
425
+
426
+ .i18n-tabs {
427
+ display: flex;
428
+ gap: 4px;
429
+ padding: 6px 8px;
430
+ border-bottom: 1px solid var(--p-surface-700, #2a2a2a);
431
+ flex-shrink: 0;
432
+ }
433
+
434
+ .i18n-tab {
435
+ background: transparent;
436
+ border: 1px solid transparent;
437
+ border-radius: 4px;
438
+ padding: 4px 10px;
439
+ color: var(--p-text-muted-color, #888);
440
+ cursor: pointer;
441
+ font-size: 11px;
442
+ display: inline-flex;
443
+ align-items: center;
444
+ gap: 6px;
445
+ }
446
+
447
+ .i18n-tab:hover {
448
+ background: var(--p-surface-800, #1f1f1f);
449
+ color: var(--p-text-color, #ddd);
450
+ }
451
+
452
+ .i18n-tab-active {
453
+ background: var(--p-surface-800, #1f1f1f);
454
+ color: var(--p-primary-color, #ec4899);
455
+ border-color: var(--p-primary-color, #ec4899);
456
+ }
457
+
458
+ .i18n-tab-count {
459
+ background: var(--p-primary-color, #ec4899);
460
+ color: #fff;
461
+ border-radius: 9px;
462
+ padding: 0 6px;
463
+ font-size: 10px;
464
+ line-height: 16px;
465
+ }
466
+
467
+ .i18n-section {
468
+ flex: 1;
469
+ overflow: auto;
470
+ padding: 10px 12px;
471
+ }
472
+
473
+ .i18n-empty {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 8px;
477
+ padding: 16px;
478
+ color: var(--p-text-muted-color, #888);
479
+ }
480
+
481
+ .i18n-row {
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 12px;
485
+ padding: 6px 0;
486
+ }
487
+
488
+ .i18n-row-label {
489
+ width: 120px;
490
+ flex-shrink: 0;
491
+ color: var(--p-text-muted-color, #888);
492
+ }
493
+
494
+ .i18n-row-value {
495
+ display: flex;
496
+ align-items: center;
497
+ gap: 6px;
498
+ flex-wrap: wrap;
499
+ }
500
+
501
+ .i18n-select,
502
+ .i18n-key-input {
503
+ background: var(--p-surface-900, #121212);
504
+ color: var(--p-text-color, #ddd);
505
+ border: 1px solid var(--p-surface-600, #404040);
506
+ border-radius: 4px;
507
+ padding: 4px 8px;
508
+ font-size: 12px;
509
+ font-family: inherit;
510
+ min-width: 0;
511
+ }
512
+
513
+ .i18n-key-input {
514
+ flex: 1;
515
+ }
516
+
517
+ .i18n-chip {
518
+ display: inline-flex;
519
+ align-items: center;
520
+ gap: 4px;
521
+ background: var(--p-surface-800, #1f1f1f);
522
+ border: 1px solid var(--p-surface-600, #404040);
523
+ border-radius: 10px;
524
+ padding: 1px 8px;
525
+ font-size: 10px;
526
+ }
527
+
528
+ .i18n-chip-hit {
529
+ border-color: #10b981;
530
+ color: #10b981;
531
+ }
532
+
533
+ .i18n-chip-miss {
534
+ border-color: #ef4444;
535
+ color: #ef4444;
536
+ }
537
+
538
+ .i18n-muted {
539
+ color: var(--p-text-muted-color, #888);
540
+ }
541
+
542
+ /* Resolve tester */
543
+ .i18n-resolve-input {
544
+ display: flex;
545
+ gap: 6px;
546
+ margin-bottom: 10px;
547
+ }
548
+
549
+ .i18n-resolve-summary {
550
+ display: flex;
551
+ align-items: center;
552
+ gap: 8px;
553
+ padding: 8px 0;
554
+ border-bottom: 1px solid var(--p-surface-700, #2a2a2a);
555
+ margin-bottom: 8px;
556
+ }
557
+
558
+ .i18n-resolve-value {
559
+ font-family: var(--font-mono, monospace);
560
+ color: var(--p-primary-color, #ec4899);
561
+ }
562
+
563
+ .i18n-trace {
564
+ list-style: none;
565
+ padding: 0;
566
+ margin: 0;
567
+ }
568
+
569
+ .i18n-trace-step {
570
+ display: flex;
571
+ align-items: flex-start;
572
+ gap: 8px;
573
+ padding: 4px 0;
574
+ font-family: var(--font-mono, monospace);
575
+ font-size: 11px;
576
+ }
577
+
578
+ .i18n-trace-kind {
579
+ width: 140px;
580
+ flex-shrink: 0;
581
+ color: var(--p-text-muted-color, #888);
582
+ }
583
+
584
+ .i18n-trace-detail {
585
+ word-break: break-all;
586
+ background: transparent;
587
+ color: var(--p-text-color, #ddd);
588
+ }
589
+
590
+ .i18n-step-miss .i18n-trace-kind { color: #ef4444; }
591
+ .i18n-step-snake-case-fallback .i18n-trace-kind { color: #f59e0b; }
592
+ .i18n-step-alias-pattern .i18n-trace-kind,
593
+ .i18n-step-alias-value .i18n-trace-kind { color: #06b6d4; }
594
+
595
+ /* Missing keys */
596
+ .i18n-missing-toolbar {
597
+ display: flex;
598
+ gap: 6px;
599
+ margin-bottom: 10px;
600
+ }
601
+
602
+ .i18n-missing-group {
603
+ margin-bottom: 12px;
604
+ }
605
+
606
+ .i18n-missing-group-header {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 6px;
610
+ margin-bottom: 4px;
611
+ font-weight: 600;
612
+ }
613
+
614
+ .i18n-missing-entry {
615
+ display: flex;
616
+ align-items: center;
617
+ gap: 8px;
618
+ padding: 3px 0 3px 20px;
619
+ font-family: var(--font-mono, monospace);
620
+ font-size: 11px;
621
+ }
622
+
623
+ .i18n-missing-key {
624
+ flex: 1;
625
+ background: transparent;
626
+ color: var(--p-text-color, #ddd);
627
+ }
628
+
629
+ .i18n-missing-time {
630
+ font-size: 10px;
631
+ }
632
+
633
+ /* Coverage matrix */
634
+ .i18n-coverage-table {
635
+ border-collapse: collapse;
636
+ width: 100%;
637
+ margin-bottom: 12px;
638
+ }
639
+
640
+ .i18n-coverage-table th,
641
+ .i18n-coverage-table td {
642
+ border: 1px solid var(--p-surface-700, #2a2a2a);
643
+ padding: 6px 10px;
644
+ text-align: center;
645
+ font-size: 11px;
646
+ }
647
+
648
+ .i18n-coverage-table th {
649
+ background: var(--p-surface-800, #1f1f1f);
650
+ color: var(--p-text-muted-color, #888);
651
+ font-weight: 500;
652
+ }
653
+
654
+ .i18n-coverage-entity {
655
+ text-align: left !important;
656
+ font-family: var(--font-mono, monospace);
657
+ }
658
+
659
+ .i18n-coverage-cell {
660
+ cursor: pointer;
661
+ transition: filter 0.1s;
662
+ }
663
+
664
+ .i18n-coverage-cell:hover {
665
+ filter: brightness(1.2);
666
+ }
667
+
668
+ .i18n-cov-green {
669
+ background: rgba(16, 185, 129, 0.15);
670
+ color: #10b981;
671
+ }
672
+
673
+ .i18n-cov-yellow {
674
+ background: rgba(245, 158, 11, 0.15);
675
+ color: #f59e0b;
676
+ }
677
+
678
+ .i18n-cov-red {
679
+ background: rgba(239, 68, 68, 0.15);
680
+ color: #ef4444;
681
+ }
682
+
683
+ .i18n-coverage-drill {
684
+ margin-top: 12px;
685
+ padding: 10px;
686
+ background: var(--p-surface-800, #1f1f1f);
687
+ border-radius: 4px;
688
+ }
689
+
690
+ .i18n-coverage-drill-header {
691
+ display: flex;
692
+ align-items: center;
693
+ gap: 8px;
694
+ margin-bottom: 8px;
695
+ }
696
+
697
+ .i18n-coverage-drill-header > .debug-toolbar-btn {
698
+ margin-left: auto;
699
+ }
700
+
701
+ .i18n-coverage-drill-list {
702
+ list-style: none;
703
+ padding: 0;
704
+ margin: 0;
705
+ font-family: var(--font-mono, monospace);
706
+ font-size: 11px;
707
+ }
708
+
709
+ .i18n-coverage-drill-list li {
710
+ display: flex;
711
+ align-items: center;
712
+ gap: 8px;
713
+ padding: 3px 0;
714
+ }
715
+
716
+ .i18n-coverage-drill-list .i18n-cov-hit i { color: #10b981; }
717
+ .i18n-coverage-drill-list .i18n-cov-miss i { color: #ef4444; }
718
+
719
+ .i18n-coverage-result {
720
+ color: var(--p-text-muted-color, #888);
721
+ }
722
+
723
+ /* Bundle inspector */
724
+ .i18n-bundle-toolbar {
725
+ display: flex;
726
+ gap: 6px;
727
+ margin-bottom: 10px;
728
+ }
729
+
730
+ .i18n-bundle-tree {
731
+ background: var(--p-surface-900, #121212);
732
+ border: 1px solid var(--p-surface-700, #2a2a2a);
733
+ border-radius: 4px;
734
+ padding: 8px;
735
+ max-height: 600px;
736
+ overflow: auto;
737
+ }
738
+ </style>