@cat-factory/app 0.47.8 → 0.47.10

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.
@@ -9,6 +9,7 @@
9
9
  // agent step and the spec-writer consume.
10
10
  import { parseOutputOutline } from '~/utils/agentOutput'
11
11
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
12
13
  import type {
13
14
  RequirementRecommendation,
14
15
  RequirementReview,
@@ -21,6 +22,7 @@ import type {
21
22
  const board = useBoardStore()
22
23
  const requirements = useRequirementsStore()
23
24
  const toast = useToast()
25
+ const { t } = useI18n()
24
26
 
25
27
  // Draft replies, keyed by item id, so editing one item doesn't disturb others.
26
28
  const drafts = ref<Record<string, string>>({})
@@ -125,6 +127,28 @@ const STATUS_COLOR = {
125
127
  recommend_requested: 'primary',
126
128
  } as const satisfies Record<ReviewItemStatus, string>
127
129
 
130
+ // Exhaustive enum→label maps of literal keys, so the typed-key drift guard sees each key
131
+ // (vs a runtime-built `requirements.severity.${value}`).
132
+ const SEVERITY_LABELS = computed<Record<ReviewItemSeverity, string>>(() => ({
133
+ high: t('requirements.severity.high'),
134
+ medium: t('requirements.severity.medium'),
135
+ low: t('requirements.severity.low'),
136
+ }))
137
+ const CATEGORY_LABELS = computed<Record<ReviewItemCategory, string>>(() => ({
138
+ gap: t('requirements.category.gap'),
139
+ clarification: t('requirements.category.clarification'),
140
+ assumption: t('requirements.category.assumption'),
141
+ risk: t('requirements.category.risk'),
142
+ question: t('requirements.category.question'),
143
+ }))
144
+ const STATUS_LABELS = computed<Record<ReviewItemStatus, string>>(() => ({
145
+ open: t('requirements.itemStatus.open'),
146
+ answered: t('requirements.itemStatus.answered'),
147
+ resolved: t('requirements.itemStatus.resolved'),
148
+ dismissed: t('requirements.itemStatus.dismissed'),
149
+ recommend_requested: t('requirements.itemStatus.recommend_requested'),
150
+ }))
151
+
128
152
  function notifyError(title: string, e: unknown) {
129
153
  toast.add({
130
154
  title,
@@ -145,7 +169,7 @@ async function persistDraft(item: RequirementReviewItem) {
145
169
  try {
146
170
  await requirements.reply(review.value, item.id, text)
147
171
  } catch (e) {
148
- notifyError('Could not save the answer', e)
172
+ notifyError(t('requirements.errors.saveAnswer'), e)
149
173
  }
150
174
  }
151
175
 
@@ -206,7 +230,7 @@ async function setStatus(item: RequirementReviewItem, itemStatus: ReviewItemStat
206
230
  try {
207
231
  await requirements.setItemStatus(review.value, item.id, itemStatus)
208
232
  } catch (e) {
209
- notifyError('Could not update the finding', e)
233
+ notifyError(t('requirements.errors.updateFinding'), e)
210
234
  }
211
235
  }
212
236
 
@@ -254,7 +278,6 @@ async function requestRecommendations() {
254
278
  const updated = await requirements.requestRecommendations(blockId.value, ids)
255
279
  markedForRecommend.value = new Set()
256
280
  const n = ids.length
257
- const plural = n === 1 ? '' : 's'
258
281
  // On a parked run the request returns at once with `pending` placeholders the durable driver
259
282
  // fills in the background; off-path (no active pipeline) there is no driver, so the Writer
260
283
  // ran inline and the recommendations are already settled. Tell the human which actually
@@ -263,18 +286,17 @@ async function requestRecommendations() {
263
286
  toast.add(
264
287
  stillGenerating
265
288
  ? {
266
- title: `Preparing ${n} recommendation${plural} in the background`,
267
- description:
268
- "Your answers are saved — close this if you like; we'll notify you when they're ready.",
289
+ title: t('requirements.toast.preparingRecommendations', { count: n }, n),
290
+ description: t('requirements.toast.preparingRecommendationsDescription'),
269
291
  icon: 'i-lucide-sparkles',
270
292
  }
271
293
  : {
272
- title: `${n} recommendation${plural} ready`,
294
+ title: t('requirements.toast.recommendationsReady', { count: n }, n),
273
295
  icon: 'i-lucide-sparkles',
274
296
  },
275
297
  )
276
298
  } catch (e) {
277
- notifyError('Could not request recommendations', e)
299
+ notifyError(t('requirements.errors.requestRecommendations'), e)
278
300
  }
279
301
  }
280
302
 
@@ -283,7 +305,7 @@ async function acceptRecommendation(rec: RequirementRecommendation) {
283
305
  try {
284
306
  await requirements.acceptRecommendation(review.value, rec.id)
285
307
  } catch (e) {
286
- notifyError('Could not accept the recommendation', e)
308
+ notifyError(t('requirements.errors.acceptRecommendation'), e)
287
309
  }
288
310
  }
289
311
 
@@ -292,7 +314,7 @@ async function rejectRecommendation(rec: RequirementRecommendation) {
292
314
  try {
293
315
  await requirements.rejectRecommendation(review.value, rec.id)
294
316
  } catch (e) {
295
- notifyError('Could not reject the recommendation', e)
317
+ notifyError(t('requirements.errors.rejectRecommendation'), e)
296
318
  }
297
319
  }
298
320
 
@@ -304,7 +326,7 @@ async function reRequestRecommendation(rec: RequirementRecommendation) {
304
326
  await requirements.reRequestRecommendation(review.value, rec.id, note)
305
327
  reRequestNotes.value = { ...reRequestNotes.value, [rec.id]: '' }
306
328
  } catch (e) {
307
- notifyError('Could not re-request the recommendation', e)
329
+ notifyError(t('requirements.errors.reRequestRecommendation'), e)
308
330
  }
309
331
  }
310
332
 
@@ -314,7 +336,7 @@ async function incorporate(feedback?: string) {
314
336
  await flushDrafts()
315
337
  await requirements.incorporate(review.value, feedback)
316
338
  } catch (e) {
317
- notifyError('Could not incorporate the answers', e)
339
+ notifyError(t('requirements.errors.incorporate'), e)
318
340
  return
319
341
  }
320
342
  redoComment.value = ''
@@ -322,8 +344,8 @@ async function incorporate(feedback?: string) {
322
344
  // The fold + re-review now run in the durable driver. Hand the user back to the board;
323
345
  // a notification calls them back only if the re-review needs more input.
324
346
  toast.add({
325
- title: 'Incorporating your answers in the background',
326
- description: "You're back on the board — we'll notify you only if more input is needed.",
347
+ title: t('requirements.toast.incorporating'),
348
+ description: t('requirements.toast.incorporatingDescription'),
327
349
  icon: 'i-lucide-wand-sparkles',
328
350
  })
329
351
  close()
@@ -333,17 +355,18 @@ async function reReview() {
333
355
  if (!blockId.value) return
334
356
  try {
335
357
  const updated = await requirements.reReview(blockId.value)
358
+ const newFindings = requirements.openCount(updated)
336
359
  toast.add({
337
360
  title:
338
361
  updated.status === 'incorporated'
339
- ? 'Reviewer is satisfied — continuing the pipeline'
362
+ ? t('requirements.toast.reviewerSatisfied')
340
363
  : updated.status === 'exceeded'
341
- ? 'Iteration limit reached — choose how to proceed'
342
- : `${requirements.openCount(updated)} new finding(s) to react to`,
364
+ ? t('requirements.toast.iterationLimitReached')
365
+ : t('requirements.toast.newFindings', { count: newFindings }, newFindings),
343
366
  icon: 'i-lucide-sparkles',
344
367
  })
345
368
  } catch (e) {
346
- notifyError('Could not re-review the requirements', e)
369
+ notifyError(t('requirements.errors.reReview'), e)
347
370
  }
348
371
  }
349
372
 
@@ -353,9 +376,9 @@ async function proceed() {
353
376
  try {
354
377
  await flushDrafts()
355
378
  await requirements.proceed(blockId.value)
356
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
379
+ toast.add({ title: t('requirements.toast.proceeding'), icon: 'i-lucide-arrow-right' })
357
380
  } catch (e) {
358
- notifyError('Could not proceed', e)
381
+ notifyError(t('requirements.errors.proceed'), e)
359
382
  } finally {
360
383
  acting.value = false
361
384
  }
@@ -367,15 +390,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
367
390
  try {
368
391
  await requirements.resolveExceeded(blockId.value, choice)
369
392
  if (choice === 'stop-reset') {
370
- toast.add({ title: 'Task reset — edit the requirements and resubmit', icon: 'i-lucide-undo' })
393
+ toast.add({ title: t('requirements.toast.taskReset'), icon: 'i-lucide-undo' })
371
394
  close()
372
395
  } else if (choice === 'proceed') {
373
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
396
+ toast.add({ title: t('requirements.toast.proceeding'), icon: 'i-lucide-arrow-right' })
374
397
  } else {
375
- toast.add({ title: 'One more review round granted', icon: 'i-lucide-rotate-cw' })
398
+ toast.add({ title: t('requirements.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
376
399
  }
377
400
  } catch (e) {
378
- notifyError('Could not resolve the review', e)
401
+ notifyError(t('requirements.errors.resolveReview'), e)
379
402
  } finally {
380
403
  acting.value = false
381
404
  }
@@ -400,12 +423,14 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
400
423
  <UIcon name="i-lucide-clipboard-check" class="h-5 w-5 text-indigo-300" />
401
424
  </div>
402
425
  <div class="min-w-0">
403
- <h1 class="truncate text-base font-semibold text-white">Requirements review</h1>
426
+ <h1 class="truncate text-base font-semibold text-white">
427
+ {{ t('requirements.title') }}
428
+ </h1>
404
429
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
405
430
  </div>
406
431
  <div class="ml-auto flex items-center gap-1.5">
407
432
  <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
408
- Iteration {{ iteration }} / {{ maxIterations }}
433
+ {{ t('requirements.iteration', { current: iteration, max: maxIterations }) }}
409
434
  </UBadge>
410
435
  <StepRestartControl
411
436
  :instance-id="instanceId"
@@ -419,13 +444,20 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
419
444
  <div class="flex min-h-0 flex-1">
420
445
  <!-- main column -->
421
446
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
422
- <p class="mb-4 text-sm text-slate-400">
423
- An AI reviewer inspected this {{ block?.level ?? 'item' }}’s collected requirements
424
- its description plus any linked PRDs and tracker issues — and raised the findings
425
- below. <span class="text-slate-300">Answer</span> the relevant ones and
426
- <span class="text-slate-300">dismiss</span> the irrelevant, then incorporate them; the
427
- reviewer re-reviews until the requirements are clear.
428
- </p>
447
+ <i18n-t
448
+ keypath="requirements.intro"
449
+ tag="p"
450
+ class="mb-4 text-sm text-slate-400"
451
+ scope="global"
452
+ >
453
+ <template #level>{{ block?.level ?? t('requirements.levelFallback') }}</template>
454
+ <template #answer
455
+ ><span class="text-slate-300">{{ t('requirements.answerVerb') }}</span></template
456
+ >
457
+ <template #dismiss
458
+ ><span class="text-slate-300">{{ t('requirements.dismissVerb') }}</span></template
459
+ >
460
+ </i18n-t>
429
461
 
430
462
  <!-- empty state — the reviewer runs automatically as the first pipeline
431
463
  gate step, so there's nothing to do here until then -->
@@ -433,8 +465,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
433
465
  v-if="!review && !busy && !loading"
434
466
  class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
435
467
  >
436
- No review yet. The reviewer runs automatically as the first step when this task's
437
- pipeline starts.
468
+ {{ t('requirements.empty') }}
438
469
  </div>
439
470
 
440
471
  <!-- working state (initial fetch on open, or a reviewer pass running) -->
@@ -443,7 +474,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
443
474
  class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
444
475
  >
445
476
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
446
- {{ loading && !busy ? 'Loading the review…' : 'Reviewing the requirements' }}
477
+ {{ loading && !busy ? t('requirements.loadingReview') : t('requirements.reviewing') }}
447
478
  </div>
448
479
 
449
480
  <template v-else-if="review">
@@ -453,16 +484,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
453
484
  class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
454
485
  >
455
486
  <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
456
- The requirements are settled. The document below is what every downstream agent step
457
- uses.
487
+ {{ t('requirements.settled') }}
458
488
  </div>
459
489
 
460
490
  <!-- iteration cap hit -->
461
491
  <IterationCapPrompt
462
492
  v-else-if="exceeded"
463
493
  class="mb-4"
464
- :heading="`Reached the ${maxIterations}-iteration limit with findings still open.`"
465
- detail="Do one more review round, proceed to the next phase with the last incorporated requirements anyway, or stop and reset the task so you can rework the requirements and resubmit."
494
+ :heading="t('requirements.exceeded.heading', { max: maxIterations })"
495
+ :detail="t('requirements.exceeded.detail')"
466
496
  :loading="acting"
467
497
  @resolve="resolveExceeded"
468
498
  />
@@ -475,12 +505,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
475
505
  >
476
506
  <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
477
507
  <span v-if="incorporating">
478
- Incorporating your answers into a requirements document… You can close this —
479
- we’ll notify you only if more input is needed.
508
+ {{ t('requirements.working.incorporating') }}
480
509
  </span>
481
510
  <span v-else>
482
- Re-reviewing the updated requirements You can close this — we’ll notify you only
483
- if more input is needed.
511
+ {{ t('requirements.working.reReviewing') }}
484
512
  </span>
485
513
  </div>
486
514
 
@@ -501,10 +529,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
501
529
  <div class="flex flex-wrap items-center gap-1.5">
502
530
  <span class="text-sm font-medium text-white">{{ item.title }}</span>
503
531
  <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
504
- {{ item.severity }}
532
+ {{ SEVERITY_LABELS[item.severity] }}
505
533
  </UBadge>
506
534
  <UBadge size="xs" variant="outline" color="neutral">
507
- {{ item.category }}
535
+ {{ CATEGORY_LABELS[item.category] }}
508
536
  </UBadge>
509
537
  <UBadge
510
538
  size="xs"
@@ -512,7 +540,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
512
540
  :color="STATUS_COLOR[item.status]"
513
541
  class="ml-auto"
514
542
  >
515
- {{ item.status }}
543
+ {{ STATUS_LABELS[item.status] }}
516
544
  </UBadge>
517
545
  </div>
518
546
  <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
@@ -526,7 +554,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
526
554
  class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
527
555
  >
528
556
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
529
- Answer
557
+ {{ t('requirements.answerLabel') }}
530
558
  </span>
531
559
  <p class="whitespace-pre-line">{{ item.reply }}</p>
532
560
  </div>
@@ -541,7 +569,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
541
569
  autoresize
542
570
  size="sm"
543
571
  class="mt-2 w-full"
544
- placeholder="Answer this finding…"
572
+ :placeholder="t('requirements.answerPlaceholder')"
545
573
  :disabled="frozen"
546
574
  @blur="persistDraft(item)"
547
575
  />
@@ -554,7 +582,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
554
582
  :disabled="frozen"
555
583
  @click="setStatus(item, 'dismissed')"
556
584
  >
557
- Dismiss as irrelevant
585
+ {{ t('requirements.dismissIrrelevant') }}
558
586
  </UButton>
559
587
  <UButton
560
588
  :color="isMarkedForRecommend(item) ? 'primary' : 'neutral'"
@@ -566,8 +594,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
566
594
  >
567
595
  {{
568
596
  isMarkedForRecommend(item)
569
- ? 'Marked for recommendation'
570
- : 'Recommend something'
597
+ ? t('requirements.markedForRecommendation')
598
+ : t('requirements.recommendSomething')
571
599
  }}
572
600
  </UButton>
573
601
  </div>
@@ -579,7 +607,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
579
607
  class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
580
608
  >
581
609
  <UIcon name="i-lucide-wand-2" class="h-3.5 w-3.5" />
582
- Recommendation requested — review the suggestion below.
610
+ {{ t('requirements.recommendationRequested') }}
583
611
  </div>
584
612
 
585
613
  <!-- reopen a dismissed finding -->
@@ -592,7 +620,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
592
620
  :disabled="frozen"
593
621
  @click="setStatus(item, 'open')"
594
622
  >
595
- Reopen
623
+ {{ t('requirements.reopen') }}
596
624
  </UButton>
597
625
  </div>
598
626
  </div>
@@ -608,13 +636,20 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
608
636
  >
609
637
  <div class="mb-3 flex items-center gap-2 text-[11px] text-indigo-300">
610
638
  <UIcon name="i-lucide-wand-2" class="h-3.5 w-3.5" />
611
- <span class="font-semibold uppercase tracking-wide">Recommended answers</span>
639
+ <span class="font-semibold uppercase tracking-wide">{{
640
+ t('requirements.recommendedAnswers')
641
+ }}</span>
612
642
  <span
613
643
  v-if="recommendationProgress"
614
644
  class="ml-auto flex items-center gap-1.5 normal-case text-indigo-300/80"
615
645
  >
616
646
  <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
617
- {{ recommendationProgress.ready }} / {{ recommendationProgress.total }} ready
647
+ {{
648
+ t('requirements.recommendationProgress', {
649
+ ready: recommendationProgress.ready,
650
+ total: recommendationProgress.total,
651
+ })
652
+ }}
618
653
  </span>
619
654
  </div>
620
655
 
@@ -633,7 +668,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
633
668
  <span class="text-sm font-medium text-white">{{
634
669
  rec.sourceFinding.title
635
670
  }}</span>
636
- <p class="text-xs text-indigo-300/70">Generating a grounded suggestion…</p>
671
+ <p class="text-xs text-indigo-300/70">
672
+ {{ t('requirements.generatingSuggestion') }}
673
+ </p>
637
674
  </div>
638
675
  </div>
639
676
  </div>
@@ -655,7 +692,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
655
692
  color="success"
656
693
  icon="i-lucide-badge-check"
657
694
  >
658
- Current standard: {{ rec.groundedInFragment.title }}
695
+ {{
696
+ t('requirements.currentStandard', { title: rec.groundedInFragment.title })
697
+ }}
659
698
  </UBadge>
660
699
  </div>
661
700
  <p class="mt-2 whitespace-pre-line text-sm text-slate-300">
@@ -670,7 +709,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
670
709
  :disabled="frozen"
671
710
  @click="acceptRecommendation(rec)"
672
711
  >
673
- Accept
712
+ {{ t('requirements.accept') }}
674
713
  </UButton>
675
714
  <UButton
676
715
  color="neutral"
@@ -680,7 +719,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
680
719
  :disabled="frozen"
681
720
  @click="rejectRecommendation(rec)"
682
721
  >
683
- Reject
722
+ {{ t('requirements.reject') }}
684
723
  </UButton>
685
724
  </div>
686
725
  <!-- re-request with a note (an alternative to rejecting outright) -->
@@ -691,7 +730,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
691
730
  autoresize
692
731
  size="sm"
693
732
  class="flex-1"
694
- placeholder="Ask for a different recommendation…"
733
+ :placeholder="t('requirements.reRequestPlaceholder')"
695
734
  :disabled="frozen || recommending"
696
735
  />
697
736
  <UButton
@@ -703,7 +742,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
703
742
  :disabled="!(reRequestNotes[rec.id] ?? '').trim() || frozen"
704
743
  @click="reRequestRecommendation(rec)"
705
744
  >
706
- Re-request
745
+ {{ t('requirements.reRequest') }}
707
746
  </UButton>
708
747
  </div>
709
748
  </div>
@@ -715,7 +754,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
715
754
  <div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
716
755
  <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
717
756
  <span class="font-semibold uppercase tracking-wide">
718
- {{ incorporated ? 'Final requirements' : 'Incorporated requirements (draft)' }}
757
+ {{
758
+ incorporated
759
+ ? t('requirements.finalRequirements')
760
+ : t('requirements.incorporatedDraft')
761
+ }}
719
762
  </span>
720
763
  </div>
721
764
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
@@ -750,19 +793,19 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
750
793
  <div class="flex flex-col gap-4 px-4 py-5">
751
794
  <div v-if="review" class="space-y-2 text-xs text-slate-400">
752
795
  <div class="flex items-center justify-between">
753
- <span>Findings</span>
796
+ <span>{{ t('requirements.stats.findings') }}</span>
754
797
  <span class="text-slate-300">{{ review.items.length }}</span>
755
798
  </div>
756
799
  <div class="flex items-center justify-between">
757
- <span>Open</span>
800
+ <span>{{ t('requirements.stats.open') }}</span>
758
801
  <span class="text-slate-300">{{ openCount }}</span>
759
802
  </div>
760
803
  <div class="flex items-center justify-between">
761
- <span>Answered</span>
804
+ <span>{{ t('requirements.stats.answered') }}</span>
762
805
  <span class="text-slate-300">{{ answeredCount }}</span>
763
806
  </div>
764
807
  <div v-if="review.model" class="flex items-center justify-between">
765
- <span>Model</span>
808
+ <span>{{ t('requirements.stats.model') }}</span>
766
809
  <span class="truncate pl-2 text-slate-500">{{ review.model }}</span>
767
810
  </div>
768
811
  </div>
@@ -781,7 +824,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
781
824
  :loading="acting"
782
825
  @click="proceed"
783
826
  >
784
- Proceed (nothing to incorporate)
827
+ {{ t('requirements.actions.proceedNothing') }}
785
828
  </UButton>
786
829
  <UButton
787
830
  v-else
@@ -793,7 +836,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
793
836
  :disabled="!canIncorporate"
794
837
  @click="incorporate()"
795
838
  >
796
- Incorporate answers
839
+ {{ t('requirements.actions.incorporateAnswers') }}
797
840
  </UButton>
798
841
  <UButton
799
842
  v-if="markedForRecommend.size > 0"
@@ -805,19 +848,22 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
805
848
  :loading="recommending"
806
849
  @click="requestRecommendations"
807
850
  >
808
- Request {{ markedForRecommend.size }} recommendation{{
809
- markedForRecommend.size === 1 ? '' : 's'
851
+ {{
852
+ t(
853
+ 'requirements.actions.requestRecommendations',
854
+ { count: markedForRecommend.size },
855
+ markedForRecommend.size,
856
+ )
810
857
  }}
811
858
  </UButton>
812
859
  <p class="text-[11px] leading-relaxed text-slate-500">
813
860
  <template v-if="canProceed">
814
- Every finding is dismissed — proceed to the next phase without reworking.
861
+ {{ t('requirements.help.canProceed') }}
815
862
  </template>
816
863
  <template v-else-if="canIncorporate">
817
- Folds your answers into one standard-format document, then re-reviews it
818
- automatically.
864
+ {{ t('requirements.help.canIncorporate') }}
819
865
  </template>
820
- <template v-else> Answer or dismiss every finding to continue. </template>
866
+ <template v-else> {{ t('requirements.help.answerAll') }} </template>
821
867
  </p>
822
868
  </div>
823
869
 
@@ -831,7 +877,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
831
877
  :loading="busy"
832
878
  @click="reReview"
833
879
  >
834
- {{ busy ? 'Re-reviewing…' : 'Looks good — re-review' }}
880
+ {{
881
+ busy
882
+ ? t('requirements.actions.reReviewing')
883
+ : t('requirements.actions.reReview')
884
+ }}
835
885
  </UButton>
836
886
  <UButton
837
887
  color="neutral"
@@ -841,7 +891,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
841
891
  icon="i-lucide-pencil"
842
892
  @click="showRedo = !showRedo"
843
893
  >
844
- Redo incorporation
894
+ {{ t('requirements.actions.redoIncorporation') }}
845
895
  </UButton>
846
896
  <div v-if="showRedo" class="space-y-2">
847
897
  <UTextarea
@@ -850,7 +900,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
850
900
  autoresize
851
901
  size="sm"
852
902
  class="w-full"
853
- placeholder="What should the merge do differently?"
903
+ :placeholder="t('requirements.redoPlaceholder')"
854
904
  />
855
905
  <UButton
856
906
  color="primary"
@@ -862,12 +912,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
862
912
  :disabled="!redoComment.trim()"
863
913
  @click="incorporate(redoComment.trim())"
864
914
  >
865
- Redo with this direction
915
+ {{ t('requirements.actions.redoWithDirection') }}
866
916
  </UButton>
867
917
  </div>
868
918
  <p class="text-[11px] leading-relaxed text-slate-500">
869
- Re-review runs the reviewer against this document. If you’re unhappy with how it
870
- was merged, redo it with a comment instead.
919
+ {{ t('requirements.help.merged') }}
871
920
  </p>
872
921
  </div>
873
922
 
@@ -875,7 +924,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
875
924
  v-if="review && incorporated"
876
925
  class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
877
926
  >
878
- Requirements settled — the pipeline is continuing with the document on the left.
927
+ {{ t('requirements.settledFooter') }}
879
928
  </div>
880
929
  </div>
881
930
  </aside>