@cat-factory/app 0.47.9 → 0.47.11

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.
@@ -8,10 +8,12 @@
8
8
  import type {
9
9
  RequirementGroup,
10
10
  RequirementItem,
11
+ RequirementKind,
11
12
  RequirementPriority,
12
13
  SpecModule,
13
14
  } from '~/types/spec'
14
15
 
16
+ const { t } = useI18n()
15
17
  const board = useBoardStore()
16
18
  const serviceSpec = useServiceSpecStore()
17
19
 
@@ -86,10 +88,19 @@ function selectGroup(m: number, g: number) {
86
88
  selected.value = { m, g }
87
89
  }
88
90
 
91
+ // Exhaustive priority → label/chip map. Literal `t()` keys keep the typed-key drift
92
+ // guard live, vs a runtime-built `spec.priority.${value}`.
89
93
  const PRIORITY_META: Record<RequirementPriority, { label: string; chip: string }> = {
90
- must: { label: 'Must', chip: 'error' },
91
- should: { label: 'Should', chip: 'warning' },
92
- could: { label: 'Could', chip: 'neutral' },
94
+ must: { label: t('spec.priority.must'), chip: 'error' },
95
+ should: { label: t('spec.priority.should'), chip: 'warning' },
96
+ could: { label: t('spec.priority.could'), chip: 'neutral' },
97
+ }
98
+
99
+ // Exhaustive requirement-kind → label map (closed union from the contracts).
100
+ const KIND_LABELS: Record<RequirementKind, string> = {
101
+ functional: t('spec.kind.functional'),
102
+ nonfunctional: t('spec.kind.nonfunctional'),
103
+ constraint: t('spec.kind.constraint'),
93
104
  }
94
105
 
95
106
  function reqCount(group: RequirementGroup): number {
@@ -98,6 +109,9 @@ function reqCount(group: RequirementGroup): number {
98
109
  function priorityMeta(item: RequirementItem) {
99
110
  return PRIORITY_META[item.priority] ?? PRIORITY_META.could
100
111
  }
112
+ function kindLabel(item: RequirementItem): string {
113
+ return KIND_LABELS[item.kind] ?? item.kind
114
+ }
101
115
  </script>
102
116
 
103
117
  <template>
@@ -118,7 +132,7 @@ function priorityMeta(item: RequirementItem) {
118
132
  <UIcon name="i-lucide-scroll-text" class="h-5 w-5 text-indigo-300" />
119
133
  </div>
120
134
  <div class="min-w-0">
121
- <h1 class="truncate text-base font-semibold text-white">Requirements</h1>
135
+ <h1 class="truncate text-base font-semibold text-white">{{ t('spec.title') }}</h1>
122
136
  <p v-if="block" class="truncate text-xs text-slate-500">
123
137
  {{ spec?.service || block.title }}
124
138
  </p>
@@ -133,7 +147,7 @@ function priorityMeta(item: RequirementItem) {
133
147
  icon="i-lucide-list-tree"
134
148
  @click="mode = 'structured'"
135
149
  >
136
- Structured
150
+ {{ t('spec.mode.structured') }}
137
151
  </UButton>
138
152
  <UButton
139
153
  :color="mode === 'gherkin' ? 'primary' : 'neutral'"
@@ -141,10 +155,10 @@ function priorityMeta(item: RequirementItem) {
141
155
  size="xs"
142
156
  icon="i-lucide-square-check-big"
143
157
  :disabled="!hasGherkin"
144
- :title="hasGherkin ? 'Gherkin scenarios' : 'No scenarios on main yet'"
158
+ :title="hasGherkin ? t('spec.mode.gherkinTooltip') : t('spec.mode.gherkinNone')"
145
159
  @click="mode = 'gherkin'"
146
160
  >
147
- Gherkin
161
+ {{ t('spec.mode.gherkin') }}
148
162
  </UButton>
149
163
  </div>
150
164
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
@@ -157,7 +171,7 @@ function priorityMeta(item: RequirementItem) {
157
171
  class="flex flex-1 items-center justify-center gap-2 text-sm text-slate-400"
158
172
  >
159
173
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
160
- Loading the specification…
174
+ {{ t('spec.loading') }}
161
175
  </div>
162
176
 
163
177
  <!-- error -->
@@ -166,7 +180,7 @@ function priorityMeta(item: RequirementItem) {
166
180
  class="flex flex-1 flex-col items-center justify-center gap-2 p-8 text-center text-sm text-slate-400"
167
181
  >
168
182
  <UIcon name="i-lucide-triangle-alert" class="h-6 w-6 text-amber-400" />
169
- Could not load the specification. Try reopening this window.
183
+ {{ t('spec.error') }}
170
184
  </div>
171
185
 
172
186
  <!-- empty: no spec on the repo's default branch yet -->
@@ -176,11 +190,9 @@ function priorityMeta(item: RequirementItem) {
176
190
  >
177
191
  <UIcon name="i-lucide-scroll-text" class="h-8 w-8 text-slate-600" />
178
192
  <div>
179
- <p class="text-sm font-medium text-slate-300">No specification yet</p>
193
+ <p class="text-sm font-medium text-slate-300">{{ t('spec.empty.title') }}</p>
180
194
  <p class="mx-auto mt-1 max-w-md text-xs text-slate-500">
181
- This service has no spec on its default branch. A specification is generated as tasks
182
- run their pipelines; once it lands on main it will appear here, with its Gherkin
183
- scenarios.
195
+ {{ t('spec.empty.description') }}
184
196
  </p>
185
197
  </div>
186
198
  </div>
@@ -198,7 +210,7 @@ function priorityMeta(item: RequirementItem) {
198
210
  icon="i-lucide-info"
199
211
  @click="selected = null"
200
212
  >
201
- Overview
213
+ {{ t('spec.overview') }}
202
214
  </UButton>
203
215
  <div v-for="(mod, mi) in modules" :key="mi" class="mb-3">
204
216
  <div
@@ -226,7 +238,7 @@ function priorityMeta(item: RequirementItem) {
226
238
  v-if="(mod.groups?.length ?? 0) === 0"
227
239
  class="px-2 py-1 text-[11px] italic text-slate-600"
228
240
  >
229
- No feature groups
241
+ {{ t('spec.noFeatureGroups') }}
230
242
  </li>
231
243
  </ul>
232
244
  </div>
@@ -240,10 +252,13 @@ function priorityMeta(item: RequirementItem) {
240
252
  <p v-if="spec?.summary" class="mt-2 whitespace-pre-line text-sm text-slate-300">
241
253
  {{ spec.summary }}
242
254
  </p>
243
- <p v-else class="mt-2 text-sm text-slate-500">No service summary.</p>
255
+ <p v-else class="mt-2 text-sm text-slate-500">{{ t('spec.noSummary') }}</p>
244
256
  <p class="mt-4 text-xs text-slate-500">
245
- {{ modules.length }} module(s). Select a feature group on the left to view its
246
- requirements{{ hasGherkin ? ' or switch to the Gherkin scenarios' : '' }}.
257
+ {{
258
+ hasGherkin
259
+ ? t('spec.moduleHintGherkin', { count: modules.length }, modules.length)
260
+ : t('spec.moduleHint', { count: modules.length }, modules.length)
261
+ }}
247
262
  </p>
248
263
  </template>
249
264
 
@@ -267,14 +282,14 @@ function priorityMeta(item: RequirementItem) {
267
282
  v-else
268
283
  class="mt-4 rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
269
284
  >
270
- No Gherkin scenarios for this group.
285
+ {{ t('spec.noGherkinForGroup') }}
271
286
  </div>
272
287
  </template>
273
288
 
274
289
  <!-- STRUCTURED view: requirements + acceptance + domain rules -->
275
290
  <template v-else>
276
291
  <div v-if="reqCount(selectedGroup) === 0" class="mt-4 text-sm text-slate-500">
277
- No requirements in this group.
292
+ {{ t('spec.noRequirements') }}
278
293
  </div>
279
294
  <ul class="mt-4 space-y-4">
280
295
  <li
@@ -288,7 +303,9 @@ function priorityMeta(item: RequirementItem) {
288
303
  <UBadge :color="priorityMeta(req).chip as any" variant="subtle" size="sm">
289
304
  {{ priorityMeta(req).label }}
290
305
  </UBadge>
291
- <UBadge color="neutral" variant="subtle" size="sm">{{ req.kind }}</UBadge>
306
+ <UBadge color="neutral" variant="subtle" size="sm">{{
307
+ kindLabel(req)
308
+ }}</UBadge>
292
309
  </div>
293
310
  </div>
294
311
  <p
@@ -304,13 +321,22 @@ function priorityMeta(item: RequirementItem) {
304
321
  class="rounded-md border border-slate-800 bg-slate-950/50 px-3 py-2 text-[12.5px] leading-relaxed"
305
322
  >
306
323
  <p class="text-slate-300">
307
- <span class="font-semibold text-emerald-400">Given</span> {{ ac.given }}
324
+ <span class="font-semibold text-emerald-400">{{
325
+ t('spec.acceptance.given')
326
+ }}</span>
327
+ {{ ac.given }}
308
328
  </p>
309
329
  <p class="text-slate-300">
310
- <span class="font-semibold text-sky-400">When</span> {{ ac.when }}
330
+ <span class="font-semibold text-sky-400">{{
331
+ t('spec.acceptance.when')
332
+ }}</span>
333
+ {{ ac.when }}
311
334
  </p>
312
335
  <p class="text-slate-300">
313
- <span class="font-semibold text-violet-400">Then</span> {{ ac.outcome }}
336
+ <span class="font-semibold text-violet-400">{{
337
+ t('spec.acceptance.then')
338
+ }}</span>
339
+ {{ ac.outcome }}
314
340
  </p>
315
341
  </div>
316
342
  </div>
@@ -323,7 +349,7 @@ function priorityMeta(item: RequirementItem) {
323
349
  class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
324
350
  >
325
351
  <UIcon name="i-lucide-shield-check" class="h-3.5 w-3.5" />
326
- Domain rules
352
+ {{ t('spec.domainRules') }}
327
353
  </div>
328
354
  <ul class="space-y-1.5">
329
355
  <li
@@ -332,9 +358,9 @@ function priorityMeta(item: RequirementItem) {
332
358
  class="rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-[13px] text-slate-300"
333
359
  >
334
360
  {{ rule.rule }}
335
- <span v-if="rule.rationale" class="text-slate-500">
336
- {{ rule.rationale }}</span
337
- >
361
+ <span v-if="rule.rationale" class="text-slate-500">{{
362
+ t('spec.ruleRationale', { rationale: rule.rationale })
363
+ }}</span>
338
364
  </li>
339
365
  </ul>
340
366
  </div>
@@ -20,6 +20,7 @@ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
20
20
 
21
21
  const board = useBoardStore()
22
22
  const execution = useExecutionStore()
23
+ const { t } = useI18n()
23
24
 
24
25
  // Per-window blob cache for the captured screenshots; revoked on unmount.
25
26
  const blobs = useArtifactBlobs()
@@ -52,11 +53,30 @@ watch(
52
53
  { immediate: true },
53
54
  )
54
55
 
55
- const STATUS_META: Record<TestOutcome['status'], { icon: string; text: string; label: string }> = {
56
- passed: { icon: 'i-lucide-circle-check', text: 'text-emerald-400', label: 'Passed' },
57
- failed: { icon: 'i-lucide-circle-x', text: 'text-rose-400', label: 'Failed' },
58
- skipped: { icon: 'i-lucide-circle-minus', text: 'text-slate-500', label: 'Skipped' },
59
- }
56
+ const STATUS_META = computed<
57
+ Record<TestOutcome['status'], { icon: string; text: string; label: string }>
58
+ >(() => ({
59
+ passed: {
60
+ icon: 'i-lucide-circle-check',
61
+ text: 'text-emerald-400',
62
+ label: t('testing.status.passed'),
63
+ },
64
+ failed: { icon: 'i-lucide-circle-x', text: 'text-rose-400', label: t('testing.status.failed') },
65
+ skipped: {
66
+ icon: 'i-lucide-circle-minus',
67
+ text: 'text-slate-500',
68
+ label: t('testing.status.skipped'),
69
+ },
70
+ }))
71
+
72
+ // Exhaustive enum→key map for the concern severity chip label (literal keys keep the
73
+ // typed-key drift guard live).
74
+ const SEVERITY_LABELS = computed<Record<TestConcern['severity'], string>>(() => ({
75
+ critical: t('testing.severity.critical'),
76
+ high: t('testing.severity.high'),
77
+ medium: t('testing.severity.medium'),
78
+ low: t('testing.severity.low'),
79
+ }))
60
80
 
61
81
  const SEVERITY_META: Record<TestConcern['severity'], { text: string; chip: string; rank: number }> =
62
82
  {
@@ -150,7 +170,7 @@ const scenarioLayout = computed<{ groups: ScenarioGroup[]; ungrouped: TestScreen
150
170
  if (leftoverOutcomes.length || leftoverConcerns.length) {
151
171
  out.push({
152
172
  key: 'other',
153
- title: r.tested.length ? 'Other checks' : 'Checks',
173
+ title: r.tested.length ? t('testing.otherChecks') : t('testing.checks'),
154
174
  other: true,
155
175
  outcomes: leftoverOutcomes,
156
176
  concerns: leftoverConcerns,
@@ -169,7 +189,7 @@ const lightboxItems = computed(() =>
169
189
  screenshots.value.map((s) => ({
170
190
  artifactId: s.artifactId,
171
191
  label: s.view,
172
- alt: `${s.view} (screenshot)`,
192
+ alt: t('testing.screenshotAlt', { view: s.view }),
173
193
  })),
174
194
  )
175
195
  const lightboxOpen = ref(false)
@@ -238,7 +258,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
238
258
  tabindex="-1"
239
259
  role="dialog"
240
260
  aria-modal="true"
241
- aria-label="Test report"
261
+ :aria-label="t('testing.title')"
242
262
  class="m-4 flex w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl focus:outline-none"
243
263
  >
244
264
  <!-- Header -->
@@ -250,10 +270,10 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
250
270
  </span>
251
271
  <div class="min-w-0 flex-1">
252
272
  <h2 class="truncate text-sm font-semibold text-slate-100">
253
- Test report{{ block ? ` — ${block.title}` : '' }}
273
+ {{ t('testing.title') }}{{ block ? ` — ${block.title}` : '' }}
254
274
  </h2>
255
275
  <p class="truncate text-[11px] text-slate-400">
256
- Exploratory + regression testing of the change
276
+ {{ t('testing.subtitle') }}
257
277
  </p>
258
278
  </div>
259
279
  <UBadge
@@ -262,15 +282,19 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
262
282
  variant="subtle"
263
283
  size="sm"
264
284
  >
265
- {{ report.greenlight ? 'Greenlit' : 'Needs fixes' }}
285
+ {{ report.greenlight ? t('testing.badge.greenlit') : t('testing.badge.needsFixes') }}
266
286
  </UBadge>
267
287
  <span
268
288
  v-if="testState && testState.attempts > 0"
269
289
  class="text-[11px] text-slate-400"
270
- :title="'Fixer attempts'"
290
+ :title="t('testing.fixerAttempts')"
271
291
  >
272
- {{ testState.attempts }}/{{ testState.maxAttempts }} fix
273
- <template v-if="testState.phase === 'fixing'"> · fixing…</template>
292
+ {{
293
+ t('testing.fixCount', { attempts: testState.attempts, max: testState.maxAttempts })
294
+ }}
295
+ <template v-if="testState.phase === 'fixing'">
296
+ {{ t('testing.fixingSuffix') }}</template
297
+ >
274
298
  </span>
275
299
  <StepRestartControl
276
300
  :instance-id="instanceId"
@@ -293,10 +317,9 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
293
317
  class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
294
318
  >
295
319
  <UIcon name="i-lucide-flask-conical" class="h-8 w-8 opacity-40" />
296
- <p class="text-sm">No test report yet.</p>
320
+ <p class="text-sm">{{ t('testing.empty.title') }}</p>
297
321
  <p class="max-w-sm text-[11px] text-slate-500">
298
- The report appears once the Tester finishes a pass. While it runs, the step shows
299
- live progress on the board.
322
+ {{ t('testing.empty.hint') }}
300
323
  </p>
301
324
  </div>
302
325
 
@@ -307,7 +330,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
307
330
  </p>
308
331
 
309
332
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
310
- Scenarios &amp; outcomes
333
+ {{ t('testing.scenariosOutcomes') }}
311
334
  </h3>
312
335
  <ul class="space-y-2">
313
336
  <li
@@ -340,12 +363,21 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
340
363
  v-if="g.screenshots.length"
341
364
  name="i-lucide-camera"
342
365
  class="h-3.5 w-3.5 shrink-0 text-slate-500"
343
- :title="`${g.screenshots.length} screenshot${g.screenshots.length === 1 ? '' : 's'}`"
366
+ :title="
367
+ t(
368
+ 'testing.screenshotCount',
369
+ { count: g.screenshots.length },
370
+ g.screenshots.length,
371
+ )
372
+ "
344
373
  />
345
374
  <span class="shrink-0 text-[11px] text-slate-500">
346
- {{ g.outcomes.length }} check{{ g.outcomes.length === 1 ? '' : 's' }}
375
+ {{ t('testing.checkCount', { count: g.outcomes.length }, g.outcomes.length) }}
347
376
  <template v-if="g.concerns.length">
348
- · {{ g.concerns.length }} concern{{ g.concerns.length === 1 ? '' : 's' }}
377
+ ·
378
+ {{
379
+ t('testing.concernCount', { count: g.concerns.length }, g.concerns.length)
380
+ }}
349
381
  </template>
350
382
  </span>
351
383
  </button>
@@ -370,7 +402,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
370
402
  </div>
371
403
  </div>
372
404
  <p v-if="!g.outcomes.length" class="py-0.5 text-[12px] italic text-slate-500">
373
- No discrete check recorded for this scenario.
405
+ {{ t('testing.noDiscreteCheck') }}
374
406
  </p>
375
407
 
376
408
  <!-- Concerns linked to this scenario -->
@@ -391,7 +423,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
391
423
  class="rounded px-1 text-[10px] uppercase"
392
424
  :class="SEVERITY_META[c.severity].chip"
393
425
  >
394
- {{ c.severity }}
426
+ {{ SEVERITY_LABELS[c.severity] }}
395
427
  </span>
396
428
  </div>
397
429
  <p v-if="c.detail" class="text-[12px] leading-snug text-slate-400">
@@ -412,14 +444,18 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
412
444
  <img
413
445
  v-if="blobs.urlFor(s.artifactId)"
414
446
  :src="blobs.urlFor(s.artifactId)"
415
- :alt="`${s.view} (screenshot)`"
447
+ :alt="t('testing.screenshotAlt', { view: s.view })"
416
448
  class="h-full w-full object-cover object-top"
417
449
  />
418
450
  <span
419
451
  v-else
420
452
  class="flex h-full w-full items-center justify-center text-[10px] text-slate-600"
421
453
  >
422
- {{ blobs.statusFor(s.artifactId) === 'error' ? 'Failed' : 'Loading…' }}
454
+ {{
455
+ blobs.statusFor(s.artifactId) === 'error'
456
+ ? t('testing.shot.failed')
457
+ : t('testing.shot.loading')
458
+ }}
423
459
  </span>
424
460
  <span
425
461
  class="absolute inset-x-0 bottom-0 truncate bg-slate-950/80 px-1 py-0.5 text-[9px] text-slate-300"
@@ -434,7 +470,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
434
470
  <!-- Standalone gallery: any captures not mapped to a scenario above -->
435
471
  <section v-if="ungroupedScreenshots.length" class="mt-5">
436
472
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
437
- Screenshots
473
+ {{ t('testing.screenshots') }}
438
474
  </h3>
439
475
  <div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
440
476
  <button
@@ -447,7 +483,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
447
483
  <img
448
484
  v-if="blobs.urlFor(s.artifactId)"
449
485
  :src="blobs.urlFor(s.artifactId)"
450
- :alt="`${s.view} (screenshot)`"
486
+ :alt="t('testing.screenshotAlt', { view: s.view })"
451
487
  class="h-full w-full object-cover object-top"
452
488
  />
453
489
  <span
@@ -455,7 +491,9 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
455
491
  class="flex h-full w-full items-center justify-center text-[11px] text-slate-600"
456
492
  >
457
493
  {{
458
- blobs.statusFor(s.artifactId) === 'error' ? 'Failed to load' : 'Loading…'
494
+ blobs.statusFor(s.artifactId) === 'error'
495
+ ? t('testing.shot.failedToLoad')
496
+ : t('testing.shot.loading')
459
497
  }}
460
498
  </span>
461
499
  <span
@@ -474,7 +512,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
474
512
  >
475
513
  <div v-if="report">
476
514
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
477
- Verdict
515
+ {{ t('testing.verdict.heading') }}
478
516
  </h4>
479
517
  <div class="flex items-center gap-2 text-[13px]">
480
518
  <UIcon
@@ -483,33 +521,39 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
483
521
  :class="report.greenlight ? 'text-emerald-400' : 'text-rose-400'"
484
522
  />
485
523
  <span :class="report.greenlight ? 'text-emerald-300' : 'text-rose-300'">
486
- {{ report.greenlight ? 'Safe to release' : 'Withheld' }}
524
+ {{
525
+ report.greenlight ? t('testing.verdict.safe') : t('testing.verdict.withheld')
526
+ }}
487
527
  </span>
488
528
  </div>
489
529
  </div>
490
530
 
491
531
  <div v-if="report">
492
532
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
493
- Outcomes
533
+ {{ t('testing.outcomes.heading') }}
494
534
  </h4>
495
535
  <dl class="space-y-1 text-[12px]">
496
536
  <div class="flex items-center justify-between">
497
- <dt class="text-slate-400">Passed</dt>
537
+ <dt class="text-slate-400">{{ t('testing.outcomes.passed') }}</dt>
498
538
  <dd class="text-emerald-300">{{ counts.passed }}</dd>
499
539
  </div>
500
540
  <div class="flex items-center justify-between">
501
- <dt class="text-slate-400">Failed</dt>
541
+ <dt class="text-slate-400">{{ t('testing.outcomes.failed') }}</dt>
502
542
  <dd class="text-rose-300">{{ counts.failed }}</dd>
503
543
  </div>
504
544
  <div class="flex items-center justify-between">
505
- <dt class="text-slate-400">Skipped</dt>
545
+ <dt class="text-slate-400">{{ t('testing.outcomes.skipped') }}</dt>
506
546
  <dd class="text-slate-300">{{ counts.skipped }}</dd>
507
547
  </div>
508
548
  <div class="flex items-center justify-between border-t border-slate-800 pt-1">
509
- <dt class="text-slate-400">Concerns</dt>
549
+ <dt class="text-slate-400">{{ t('testing.outcomes.concerns') }}</dt>
510
550
  <dd class="text-amber-300">
511
551
  {{ counts.concerns
512
- }}<template v-if="counts.blocking"> ({{ counts.blocking }} blocking)</template>
552
+ }}<template v-if="counts.blocking">
553
+ {{
554
+ t('testing.outcomes.blocking', { count: counts.blocking }, counts.blocking)
555
+ }}</template
556
+ >
513
557
  </dd>
514
558
  </div>
515
559
  </dl>
@@ -517,7 +561,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
517
561
 
518
562
  <div v-if="report?.environment">
519
563
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
520
- Environment
564
+ {{ t('testing.environment') }}
521
565
  </h4>
522
566
  <p class="text-[12px] capitalize text-slate-300">{{ report.environment }}</p>
523
567
  </div>
@@ -535,8 +579,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
535
579
  />
536
580
 
537
581
  <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
538
- Scenarios are the areas the Tester chose to exercise (its spec acceptance scenarios).
539
- Outcomes and concerns are grouped under them by name.
582
+ {{ t('testing.footer') }}
540
583
  </p>
541
584
  </aside>
542
585
  </div>