@cat-factory/app 0.46.12 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/app/components/auth/AuthGate.vue +1 -1
  2. package/app/components/auth/LoginScreen.vue +131 -1
  3. package/app/components/layout/AccountDeploymentSettings.vue +232 -5
  4. package/app/components/providers/ApiKeysSection.vue +27 -3
  5. package/app/components/providers/PersonalSubscriptionSection.vue +17 -2
  6. package/app/components/providers/SignInRequiredNotice.vue +19 -0
  7. package/app/components/settings/AccountSettingsPanel.vue +9 -6
  8. package/app/components/settings/IssueTrackerPanel.vue +114 -53
  9. package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
  10. package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
  11. package/app/components/settings/MergeThresholdsPanel.vue +93 -45
  12. package/app/components/settings/ModelConfigurationPanel.vue +62 -36
  13. package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
  14. package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
  15. package/app/components/settings/ProviderConnectionPanel.vue +115 -61
  16. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
  17. package/app/components/settings/UserSecretsSection.vue +37 -18
  18. package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
  19. package/app/composables/api/auth.ts +7 -0
  20. package/app/stores/auth.ts +25 -1
  21. package/app/types/accountSettings.ts +4 -0
  22. package/i18n/locales/en.json +496 -0
  23. package/i18n/locales/es.json +490 -0
  24. package/i18n/locales/fr.json +490 -0
  25. package/i18n/locales/pl.json +490 -0
  26. package/i18n/locales/uk.json +490 -0
  27. package/package.json +2 -2
@@ -9,6 +9,7 @@ import { computed, ref, watch } from 'vue'
9
9
  import { LOCAL_RUNNER_DEFAULTS, LOCAL_RUNNER_LABELS, type LocalRunner } from '~/types/localModels'
10
10
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
11
11
 
12
+ const { t } = useI18n()
12
13
  const ui = useUiStore()
13
14
  const store = useLocalModelsStore()
14
15
  const toast = useToast()
@@ -95,7 +96,7 @@ async function test() {
95
96
  selected.value = keep.length ? keep : [...result.models]
96
97
  testError.value = null
97
98
  } else {
98
- testError.value = result.error ?? 'Could not reach the runner.'
99
+ testError.value = result.error ?? t('settings.localModelEndpoints.unreachable')
99
100
  }
100
101
  } catch (e) {
101
102
  testError.value = e instanceof Error ? e.message : String(e)
@@ -125,12 +126,14 @@ async function save() {
125
126
  })
126
127
  apiKey.value = ''
127
128
  toast.add({
128
- title: `${LOCAL_RUNNER_LABELS[provider.value]} saved`,
129
+ title: t('settings.localModelEndpoints.toast.saved', {
130
+ name: LOCAL_RUNNER_LABELS[provider.value],
131
+ }),
129
132
  icon: 'i-lucide-check',
130
133
  color: 'success',
131
134
  })
132
135
  } catch (e) {
133
- notifyError('Could not save runner', e)
136
+ notifyError(t('settings.localModelEndpoints.toast.saveFailed'), e)
134
137
  } finally {
135
138
  busy.value = false
136
139
  }
@@ -147,9 +150,9 @@ async function remove(p: LocalRunner) {
147
150
  selected.value = []
148
151
  tested.value = false
149
152
  }
150
- toast.add({ title: 'Runner removed', icon: 'i-lucide-check' })
153
+ toast.add({ title: t('settings.localModelEndpoints.toast.removed'), icon: 'i-lucide-check' })
151
154
  } catch (e) {
152
- notifyError('Could not remove runner', e)
155
+ notifyError(t('settings.localModelEndpoints.toast.removeFailed'), e)
153
156
  } finally {
154
157
  busy.value = false
155
158
  }
@@ -157,18 +160,27 @@ async function remove(p: LocalRunner) {
157
160
  </script>
158
161
 
159
162
  <template>
160
- <UModal v-model:open="open" title="My local runners" :ui="{ content: 'max-w-2xl' }">
163
+ <UModal
164
+ v-model:open="open"
165
+ :title="t('settings.localModelEndpoints.title')"
166
+ :ui="{ content: 'max-w-2xl' }"
167
+ >
161
168
  <template #title>
162
- <IntegrationBackTitle title="My local runners" @back="back" />
169
+ <IntegrationBackTitle :title="t('settings.localModelEndpoints.title')" @back="back" />
163
170
  </template>
164
171
  <template #body>
165
172
  <div class="space-y-4">
166
173
  <p class="text-xs text-slate-400">
167
- Point agents at an LLM running on <strong>your own machine</strong> — Ollama, LM Studio,
168
- llama.cpp, vLLM, or any OpenAI-compatible server. A runner is stored
169
- <span class="text-slate-300">just for you</span> (a runner lives on your box), and the
170
- models you enable appear automatically in the model picker. The API key (most runners
171
- ignore it) is write-only and never shown again.
174
+ <i18n-t keypath="settings.localModelEndpoints.intro" tag="span" scope="global">
175
+ <template #ownMachine>
176
+ <strong>{{ t('settings.localModelEndpoints.introOwnMachine') }}</strong>
177
+ </template>
178
+ <template #justForYou>
179
+ <span class="text-slate-300">{{
180
+ t('settings.localModelEndpoints.introJustForYou')
181
+ }}</span>
182
+ </template>
183
+ </i18n-t>
172
184
  </p>
173
185
 
174
186
  <!-- connected endpoints -->
@@ -181,8 +193,17 @@ async function remove(p: LocalRunner) {
181
193
  <span class="font-medium text-slate-200">{{ e.label }}</span>
182
194
  <span class="ml-2 text-xs text-slate-500">{{ LOCAL_RUNNER_LABELS[e.provider] }}</span>
183
195
  <div class="text-[11px] text-slate-500">
184
- {{ e.baseUrl }} · {{ e.models.length }} model{{ e.models.length === 1 ? '' : 's' }}
185
- <template v-if="e.hasApiKey"> · key set</template>
196
+ {{ e.baseUrl }} ·
197
+ {{
198
+ t(
199
+ 'settings.localModelEndpoints.modelCount',
200
+ { count: e.models.length },
201
+ e.models.length,
202
+ )
203
+ }}
204
+ <template v-if="e.hasApiKey">
205
+ · {{ t('settings.localModelEndpoints.keySet') }}</template
206
+ >
186
207
  </div>
187
208
  </div>
188
209
  <div class="flex items-center gap-1">
@@ -192,7 +213,7 @@ async function remove(p: LocalRunner) {
192
213
  variant="ghost"
193
214
  size="xs"
194
215
  :disabled="busy"
195
- title="Edit"
216
+ :title="t('settings.localModelEndpoints.edit')"
196
217
  @click="provider = e.provider"
197
218
  />
198
219
  <UButton
@@ -209,29 +230,45 @@ async function remove(p: LocalRunner) {
209
230
  <!-- add / edit form -->
210
231
  <div class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3">
211
232
  <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
212
- {{ existing ? 'Edit runner' : 'Add a runner' }}
233
+ {{
234
+ existing
235
+ ? t('settings.localModelEndpoints.editRunner')
236
+ : t('settings.localModelEndpoints.addRunner')
237
+ }}
213
238
  </p>
214
239
 
215
240
  <div class="flex flex-wrap items-end gap-3">
216
- <UFormField label="Runner type">
241
+ <UFormField :label="t('settings.localModelEndpoints.runnerType')">
217
242
  <USelect v-model="provider" :items="RUNNERS" value-key="value" class="w-48" />
218
243
  </UFormField>
219
- <UFormField label="Label (optional)" class="flex-1 min-w-40">
220
- <UInput v-model="label" :placeholder="`My ${LOCAL_RUNNER_LABELS[provider]}`" />
244
+ <UFormField
245
+ :label="t('settings.localModelEndpoints.labelOptional')"
246
+ class="flex-1 min-w-40"
247
+ >
248
+ <UInput
249
+ v-model="label"
250
+ :placeholder="
251
+ t('settings.localModelEndpoints.labelPlaceholder', {
252
+ name: LOCAL_RUNNER_LABELS[provider],
253
+ })
254
+ "
255
+ />
221
256
  </UFormField>
222
257
  </div>
223
258
 
224
- <UFormField label="Base URL">
259
+ <UFormField :label="t('settings.localModelEndpoints.baseUrl')">
225
260
  <UInput v-model="baseUrl" class="font-mono" placeholder="http://localhost:11434/v1" />
226
261
  </UFormField>
227
262
 
228
- <UFormField label="API key (optional)">
263
+ <UFormField :label="t('settings.localModelEndpoints.apiKeyOptional')">
229
264
  <UInput
230
265
  v-model="apiKey"
231
266
  type="password"
232
267
  class="font-mono"
233
268
  :placeholder="
234
- existing?.hasApiKey ? 'leave blank to keep stored key' : 'most runners ignore this'
269
+ existing?.hasApiKey
270
+ ? t('settings.localModelEndpoints.apiKeyKeepPlaceholder')
271
+ : t('settings.localModelEndpoints.apiKeyIgnorePlaceholder')
235
272
  "
236
273
  />
237
274
  </UFormField>
@@ -246,19 +283,27 @@ async function remove(p: LocalRunner) {
246
283
  :disabled="!baseUrl.trim()"
247
284
  @click="test()"
248
285
  >
249
- Test connection
286
+ {{ t('settings.localModelEndpoints.testConnection') }}
250
287
  </UButton>
251
288
  <span v-if="testError" class="text-xs text-rose-400">{{ testError }}</span>
252
289
  <span v-else-if="tested && discovered.length" class="text-xs text-emerald-400">
253
- Reachable · {{ discovered.length }} model{{ discovered.length === 1 ? '' : 's' }}
290
+ {{
291
+ t(
292
+ 'settings.localModelEndpoints.reachable',
293
+ { count: discovered.length },
294
+ discovered.length,
295
+ )
296
+ }}
254
297
  </span>
255
- <span v-else-if="tested" class="text-xs text-slate-500">No models reported.</span>
298
+ <span v-else-if="tested" class="text-xs text-slate-500">{{
299
+ t('settings.localModelEndpoints.noModels')
300
+ }}</span>
256
301
  </div>
257
302
 
258
303
  <!-- discovered models multi-select -->
259
304
  <div v-if="discovered.length" class="space-y-1.5">
260
305
  <span class="block text-[10px] uppercase tracking-wide text-slate-500">
261
- Enable models
306
+ {{ t('settings.localModelEndpoints.enableModels') }}
262
307
  </span>
263
308
  <div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
264
309
  <label
@@ -285,7 +330,7 @@ async function remove(p: LocalRunner) {
285
330
  :disabled="!baseUrl.trim() || !selected.length"
286
331
  @click="save()"
287
332
  >
288
- {{ existing ? 'Save' : 'Add runner' }}
333
+ {{ existing ? t('common.save') : t('settings.localModelEndpoints.addRunner') }}
289
334
  </UButton>
290
335
  </div>
291
336
  </div>
@@ -4,16 +4,28 @@
4
4
  // resolved preset). Full CRUD over the mergePresets store — the same library the
5
5
  // task inspector's "Merge policy" dropdown selects from. Exactly one preset is the
6
6
  // default; it cannot be deleted or un-defaulted (the backend enforces this too).
7
- import { reactive, ref, watch } from 'vue'
7
+ import { computed, reactive, ref, watch } from 'vue'
8
8
  import type { MergeThresholdPreset, RequirementConcernLevel } from '~/types/merge'
9
9
 
10
+ const { t } = useI18n()
11
+
12
+ // Per-concern-level label. An exhaustive Record keyed off the union (a missing member fails
13
+ // the typecheck); each value is a LITERAL catalog key so the typed-message-keys check sees
14
+ // it. Leaf keys mirror the enum value verbatim.
15
+ const CONCERN_LABEL_KEYS: Record<RequirementConcernLevel, string> = {
16
+ none: 'settings.mergeThresholds.concern.none',
17
+ low: 'settings.mergeThresholds.concern.low',
18
+ medium: 'settings.mergeThresholds.concern.medium',
19
+ high: 'settings.mergeThresholds.concern.high',
20
+ }
21
+
10
22
  // Concern-level options for the requirements auto-pass threshold (none < low < medium < high).
11
- const CONCERN_LEVELS: { value: RequirementConcernLevel; label: string }[] = [
12
- { value: 'none', label: 'None (always stop)' },
13
- { value: 'low', label: 'Low' },
14
- { value: 'medium', label: 'Medium' },
15
- { value: 'high', label: 'High (never stop)' },
16
- ]
23
+ const CONCERN_LEVELS = computed<{ value: RequirementConcernLevel; label: string }[]>(() => [
24
+ { value: 'none', label: t(CONCERN_LABEL_KEYS.none) },
25
+ { value: 'low', label: t(CONCERN_LABEL_KEYS.low) },
26
+ { value: 'medium', label: t(CONCERN_LABEL_KEYS.medium) },
27
+ { value: 'high', label: t(CONCERN_LABEL_KEYS.high) },
28
+ ])
17
29
 
18
30
  const store = useMergePresetsStore()
19
31
  const toast = useToast()
@@ -77,9 +89,13 @@ async function save(p: MergeThresholdPreset) {
77
89
  maxRequirementIterations: d.maxRequirementIterations,
78
90
  maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
79
91
  })
80
- toast.add({ title: 'Preset saved', icon: 'i-lucide-check', color: 'success' })
92
+ toast.add({
93
+ title: t('settings.mergeThresholds.toast.saved'),
94
+ icon: 'i-lucide-check',
95
+ color: 'success',
96
+ })
81
97
  } catch (e) {
82
- notifyError('Could not save preset', e)
98
+ notifyError(t('settings.mergeThresholds.toast.saveFailed'), e)
83
99
  } finally {
84
100
  busy.value = null
85
101
  }
@@ -90,7 +106,7 @@ async function makeDefault(p: MergeThresholdPreset) {
90
106
  try {
91
107
  await store.update(p.id, { isDefault: true })
92
108
  } catch (e) {
93
- notifyError('Could not set default', e)
109
+ notifyError(t('settings.mergeThresholds.toast.defaultFailed'), e)
94
110
  } finally {
95
111
  busy.value = null
96
112
  }
@@ -101,7 +117,7 @@ async function remove(p: MergeThresholdPreset) {
101
117
  try {
102
118
  await store.remove(p.id)
103
119
  } catch (e) {
104
- notifyError('Could not delete preset', e)
120
+ notifyError(t('settings.mergeThresholds.toast.deleteFailed'), e)
105
121
  } finally {
106
122
  busy.value = null
107
123
  }
@@ -133,9 +149,13 @@ async function create() {
133
149
  maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
134
150
  })
135
151
  draft.name = ''
136
- toast.add({ title: 'Preset created', icon: 'i-lucide-check', color: 'success' })
152
+ toast.add({
153
+ title: t('settings.mergeThresholds.toast.created'),
154
+ icon: 'i-lucide-check',
155
+ color: 'success',
156
+ })
137
157
  } catch (e) {
138
- notifyError('Could not create preset', e)
158
+ notifyError(t('settings.mergeThresholds.toast.createFailed'), e)
139
159
  } finally {
140
160
  creating.value = false
141
161
  }
@@ -144,13 +164,16 @@ async function create() {
144
164
 
145
165
  <template>
146
166
  <div class="space-y-4">
147
- <p class="text-xs text-slate-400">
148
- Named auto-merge policies a task can choose. After CI passes, the
149
- <span class="text-slate-300">merger</span> agent scores the PR on complexity, risk and impact
150
- (0–100%); the PR auto-merges only when every score is at or below the preset's ceilings —
151
- otherwise a review notification is raised. The default preset governs any task that picks
152
- none.
153
- </p>
167
+ <i18n-t
168
+ keypath="settings.mergeThresholds.intro"
169
+ tag="p"
170
+ class="text-xs text-slate-400"
171
+ scope="global"
172
+ >
173
+ <template #merger>
174
+ <span class="text-slate-300">{{ t('settings.mergeThresholds.mergerAgent') }}</span>
175
+ </template>
176
+ </i18n-t>
154
177
 
155
178
  <div
156
179
  v-for="p in store.presets"
@@ -158,8 +181,15 @@ async function create() {
158
181
  class="rounded-lg border border-slate-700 bg-slate-800/40 p-3"
159
182
  >
160
183
  <div class="mb-3 flex items-center gap-2">
161
- <UInput v-model="drafts[p.id]!.name" size="sm" class="flex-1" placeholder="Preset name" />
162
- <UBadge v-if="p.isDefault" color="primary" variant="subtle" size="sm">Default</UBadge>
184
+ <UInput
185
+ v-model="drafts[p.id]!.name"
186
+ size="sm"
187
+ class="flex-1"
188
+ :placeholder="t('settings.mergeThresholds.presetNamePlaceholder')"
189
+ />
190
+ <UBadge v-if="p.isDefault" color="primary" variant="subtle" size="sm">
191
+ {{ t('settings.mergeThresholds.default') }}
192
+ </UBadge>
163
193
  <UButton
164
194
  v-else
165
195
  color="neutral"
@@ -169,7 +199,7 @@ async function create() {
169
199
  :loading="busy === p.id"
170
200
  @click="makeDefault(p)"
171
201
  >
172
- Make default
202
+ {{ t('settings.mergeThresholds.makeDefault') }}
173
203
  </UButton>
174
204
  <UButton
175
205
  color="error"
@@ -177,7 +207,11 @@ async function create() {
177
207
  size="xs"
178
208
  icon="i-lucide-trash-2"
179
209
  :disabled="p.isDefault || busy === p.id"
180
- :title="p.isDefault ? 'The default preset cannot be deleted' : 'Delete preset'"
210
+ :title="
211
+ p.isDefault
212
+ ? t('settings.mergeThresholds.deleteDefaultBlocked')
213
+ : t('settings.mergeThresholds.deletePreset')
214
+ "
181
215
  @click="remove(p)"
182
216
  />
183
217
  </div>
@@ -185,7 +219,7 @@ async function create() {
185
219
  <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
186
220
  <label class="block">
187
221
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
188
- Max complexity %
222
+ {{ t('settings.mergeThresholds.field.maxComplexity') }}
189
223
  </span>
190
224
  <UInput
191
225
  v-model.number="drafts[p.id]!.maxComplexity"
@@ -197,7 +231,7 @@ async function create() {
197
231
  </label>
198
232
  <label class="block">
199
233
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
200
- Max risk %
234
+ {{ t('settings.mergeThresholds.field.maxRisk') }}
201
235
  </span>
202
236
  <UInput
203
237
  v-model.number="drafts[p.id]!.maxRisk"
@@ -209,7 +243,7 @@ async function create() {
209
243
  </label>
210
244
  <label class="block">
211
245
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
212
- Max impact %
246
+ {{ t('settings.mergeThresholds.field.maxImpact') }}
213
247
  </span>
214
248
  <UInput
215
249
  v-model.number="drafts[p.id]!.maxImpact"
@@ -221,7 +255,7 @@ async function create() {
221
255
  </label>
222
256
  <label class="block">
223
257
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
224
- CI-fix attempts
258
+ {{ t('settings.mergeThresholds.field.ciMaxAttempts') }}
225
259
  </span>
226
260
  <UInput
227
261
  v-model.number="drafts[p.id]!.ciMaxAttempts"
@@ -233,7 +267,7 @@ async function create() {
233
267
  </label>
234
268
  <label class="block">
235
269
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
236
- Requirement iterations
270
+ {{ t('settings.mergeThresholds.field.maxRequirementIterations') }}
237
271
  </span>
238
272
  <UInput
239
273
  v-model.number="drafts[p.id]!.maxRequirementIterations"
@@ -245,7 +279,7 @@ async function create() {
245
279
  </label>
246
280
  <label class="block">
247
281
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
248
- Auto-pass concerns
282
+ {{ t('settings.mergeThresholds.field.maxRequirementConcernAllowed') }}
249
283
  </span>
250
284
  <USelect
251
285
  v-model="drafts[p.id]!.maxRequirementConcernAllowed"
@@ -265,7 +299,7 @@ async function create() {
265
299
  :loading="busy === p.id"
266
300
  @click="save(p)"
267
301
  >
268
- Save
302
+ {{ t('common.save') }}
269
303
  </UButton>
270
304
  </div>
271
305
  </div>
@@ -273,15 +307,23 @@ async function create() {
273
307
  <!-- create -->
274
308
  <div class="rounded-lg border border-dashed border-slate-700 p-3">
275
309
  <p class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
276
- New preset
310
+ {{ t('settings.mergeThresholds.newPreset') }}
277
311
  </p>
278
312
  <div class="flex flex-wrap items-end gap-3">
279
313
  <label class="block min-w-40 flex-1">
280
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">Name</span>
281
- <UInput v-model="draft.name" size="sm" placeholder="e.g. Cautious" />
314
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
315
+ {{ t('settings.mergeThresholds.create.name') }}
316
+ </span>
317
+ <UInput
318
+ v-model="draft.name"
319
+ size="sm"
320
+ :placeholder="t('settings.mergeThresholds.create.namePlaceholder')"
321
+ />
282
322
  </label>
283
323
  <label class="block w-20">
284
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">Cmplx%</span>
324
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
325
+ {{ t('settings.mergeThresholds.create.complexity') }}
326
+ </span>
285
327
  <UInput
286
328
  v-model.number="draft.maxComplexity"
287
329
  type="number"
@@ -291,21 +333,27 @@ async function create() {
291
333
  />
292
334
  </label>
293
335
  <label class="block w-20">
294
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">Risk%</span>
336
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
337
+ {{ t('settings.mergeThresholds.create.risk') }}
338
+ </span>
295
339
  <UInput v-model.number="draft.maxRisk" type="number" :min="0" :max="100" size="sm" />
296
340
  </label>
297
341
  <label class="block w-20">
298
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">Impact%</span>
342
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
343
+ {{ t('settings.mergeThresholds.create.impact') }}
344
+ </span>
299
345
  <UInput v-model.number="draft.maxImpact" type="number" :min="0" :max="100" size="sm" />
300
346
  </label>
301
347
  <label class="block w-20">
302
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">CI-fix</span>
348
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
349
+ {{ t('settings.mergeThresholds.create.ciFix') }}
350
+ </span>
303
351
  <UInput v-model.number="draft.ciMaxAttempts" type="number" :min="0" :max="50" size="sm" />
304
352
  </label>
305
353
  <label class="block w-20">
306
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
307
- >Req iter</span
308
- >
354
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
355
+ {{ t('settings.mergeThresholds.create.reqIter') }}
356
+ </span>
309
357
  <UInput
310
358
  v-model.number="draft.maxRequirementIterations"
311
359
  type="number"
@@ -315,9 +363,9 @@ async function create() {
315
363
  />
316
364
  </label>
317
365
  <label class="block w-32">
318
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
319
- >Auto-pass ≤</span
320
- >
366
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
367
+ {{ t('settings.mergeThresholds.create.autoPass') }}
368
+ </span>
321
369
  <USelect
322
370
  v-model="draft.maxRequirementConcernAllowed"
323
371
  :items="CONCERN_LEVELS"
@@ -333,7 +381,7 @@ async function create() {
333
381
  :disabled="!draft.name.trim()"
334
382
  @click="create"
335
383
  >
336
- Add
384
+ {{ t('settings.mergeThresholds.add') }}
337
385
  </UButton>
338
386
  </div>
339
387
  </div>