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