@cat-factory/app 0.46.11 → 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 (42) hide show
  1. package/app/components/auth/AuthGate.vue +3 -2
  2. package/app/components/auth/LoginScreen.vue +172 -32
  3. package/app/components/auth/ResetPasswordScreen.vue +16 -13
  4. package/app/components/auth/UserMenu.vue +3 -2
  5. package/app/components/layout/AccountDeploymentSettings.vue +307 -37
  6. package/app/components/layout/AccountFragmentSettings.vue +2 -3
  7. package/app/components/layout/AccountTeamSettings.vue +75 -45
  8. package/app/components/layout/AiProvidersBanner.vue +11 -11
  9. package/app/components/layout/BoardSwitcher.vue +63 -29
  10. package/app/components/layout/CommandBar.vue +68 -54
  11. package/app/components/layout/GitHubPatBanner.vue +13 -6
  12. package/app/components/layout/IntegrationBackTitle.vue +4 -1
  13. package/app/components/layout/IntegrationsHub.vue +67 -49
  14. package/app/components/layout/NotificationsInbox.vue +48 -19
  15. package/app/components/layout/PersonalSetupModal.vue +26 -16
  16. package/app/components/layout/ProviderConfigBanner.vue +14 -9
  17. package/app/components/layout/SideBar.vue +20 -18
  18. package/app/components/layout/SpendWarningBanner.vue +16 -16
  19. package/app/components/providers/ApiKeysSection.vue +27 -3
  20. package/app/components/providers/PersonalSubscriptionSection.vue +18 -4
  21. package/app/components/providers/SignInRequiredNotice.vue +19 -0
  22. package/app/components/settings/AccountSettingsPanel.vue +9 -6
  23. package/app/components/settings/IssueTrackerPanel.vue +114 -53
  24. package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
  25. package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
  26. package/app/components/settings/MergeThresholdsPanel.vue +93 -45
  27. package/app/components/settings/ModelConfigurationPanel.vue +62 -36
  28. package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
  29. package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
  30. package/app/components/settings/ProviderConnectionPanel.vue +115 -61
  31. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
  32. package/app/components/settings/UserSecretsSection.vue +37 -18
  33. package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
  34. package/app/composables/api/auth.ts +7 -0
  35. package/app/stores/auth.ts +25 -1
  36. package/app/types/accountSettings.ts +4 -0
  37. package/i18n/locales/en.json +908 -0
  38. package/i18n/locales/es.json +899 -0
  39. package/i18n/locales/fr.json +899 -0
  40. package/i18n/locales/pl.json +899 -0
  41. package/i18n/locales/uk.json +899 -0
  42. package/package.json +2 -2
@@ -17,6 +17,7 @@ import type { ModelPreset } from '~/types/model-presets'
17
17
  import { MODEL_CONFIGURABLE_SYSTEM_KINDS } from '~/utils/catalog'
18
18
  import { cachingLabel, contextLabel, costLabel, displayFlavor, isSelectable } from '~/stores/models'
19
19
 
20
+ const { t } = useI18n()
20
21
  const ui = useUiStore()
21
22
  const models = useModelsStore()
22
23
  const presets = useModelPresetsStore()
@@ -83,7 +84,9 @@ const selectableModels = computed(() => {
83
84
  .map((m) => {
84
85
  const flavor = displayFlavor(m, configured)
85
86
  const ctx = contextLabel(flavor.contextTokens)
86
- const price = costLabel(flavor) ?? (flavor.quotaBased ? 'quota' : undefined)
87
+ const price =
88
+ costLabel(flavor) ??
89
+ (flavor.quotaBased ? t('settings.modelConfiguration.quota') : undefined)
87
90
  // Surface caching in the suffix: a cache-less flavour (the Workers-AI hot path)
88
91
  // re-bills its whole growing prompt every turn, which the user can act on.
89
92
  const caching = cachingLabel(flavor)
@@ -134,7 +137,7 @@ async function setDefault(p: ModelPreset) {
134
137
  try {
135
138
  await presets.update(p.id, { isDefault: true })
136
139
  } catch (e) {
137
- fail('Could not set the default preset', e)
140
+ fail(t('settings.modelConfiguration.toast.setDefaultFailed'), e)
138
141
  } finally {
139
142
  busy.value = false
140
143
  }
@@ -145,7 +148,7 @@ async function remove(p: ModelPreset) {
145
148
  try {
146
149
  await presets.remove(p.id)
147
150
  } catch (e) {
148
- fail('Could not delete the preset', e)
151
+ fail(t('settings.modelConfiguration.toast.deleteFailed'), e)
149
152
  } finally {
150
153
  busy.value = false
151
154
  }
@@ -168,7 +171,7 @@ function overrideMenu(kind: AgentKind) {
168
171
  return [
169
172
  [
170
173
  {
171
- label: 'Use base model',
174
+ label: t('settings.modelConfiguration.editor.useBaseModel'),
172
175
  icon: 'i-lucide-rotate-ccw',
173
176
  onSelect: () => setOverride(kind, null),
174
177
  },
@@ -190,14 +193,17 @@ function setOverride(kind: AgentKind, modelId: string | null) {
190
193
  /** The label on a kind's override button: its override model, else "Base model". */
191
194
  function overrideLabel(kind: AgentKind): string {
192
195
  const id = editor.value?.overrides[kind]
193
- return id ? modelLabel(id) : 'Base model'
196
+ return id ? modelLabel(id) : t('settings.modelConfiguration.editor.baseModel')
194
197
  }
195
198
 
196
199
  async function save() {
197
200
  const e = editor.value
198
201
  if (!e) return
199
202
  if (!e.name.trim()) {
200
- fail('Name required', new Error('Give the preset a name.'))
203
+ fail(
204
+ t('settings.modelConfiguration.toast.nameRequiredTitle'),
205
+ new Error(t('settings.modelConfiguration.toast.nameRequiredBody')),
206
+ )
201
207
  return
202
208
  }
203
209
  busy.value = true
@@ -219,7 +225,7 @@ async function save() {
219
225
  }
220
226
  editor.value = null
221
227
  } catch (err) {
222
- fail('Could not save the preset', err)
228
+ fail(t('settings.modelConfiguration.toast.saveFailed'), err)
223
229
  } finally {
224
230
  busy.value = false
225
231
  }
@@ -251,10 +257,11 @@ function fail(title: string, e: unknown) {
251
257
  <UIcon name="i-lucide-cpu" class="h-5 w-5 text-indigo-300" />
252
258
  </div>
253
259
  <div class="min-w-0">
254
- <h1 class="truncate text-base font-semibold text-white">Model Configuration</h1>
260
+ <h1 class="truncate text-base font-semibold text-white">
261
+ {{ t('settings.modelConfiguration.title') }}
262
+ </h1>
255
263
  <p class="truncate text-xs text-slate-500">
256
- Presets that map a model to every agent. A task picks one; tasks default to the
257
- workspace default preset.
264
+ {{ t('settings.modelConfiguration.subtitle') }}
258
265
  </p>
259
266
  </div>
260
267
  <UButton
@@ -266,7 +273,7 @@ function fail(title: string, e: unknown) {
266
273
  class="ml-auto"
267
274
  @click="editor = null"
268
275
  >
269
- Back
276
+ {{ t('settings.modelConfiguration.back') }}
270
277
  </UButton>
271
278
  <UButton
272
279
  icon="i-lucide-x"
@@ -274,7 +281,7 @@ function fail(title: string, e: unknown) {
274
281
  variant="ghost"
275
282
  size="sm"
276
283
  :class="editor ? '' : 'ml-auto'"
277
- title="Close (Esc)"
284
+ :title="t('settings.modelConfiguration.closeEsc')"
278
285
  @click="open = false"
279
286
  />
280
287
  </header>
@@ -285,9 +292,17 @@ function fail(title: string, e: unknown) {
285
292
  <template v-if="!editor">
286
293
  <div class="flex items-center justify-between">
287
294
  <p class="text-sm leading-relaxed text-slate-400">
288
- Each preset sets a <span class="text-slate-300">base model</span> for every agent,
289
- with optional per-agent overrides. A model pinned on an individual task still
290
- overrides the preset.
295
+ <i18n-t
296
+ keypath="settings.modelConfiguration.list.intro"
297
+ scope="global"
298
+ tag="span"
299
+ >
300
+ <template #baseModel>
301
+ <span class="text-slate-300">{{
302
+ t('settings.modelConfiguration.list.introBaseModel')
303
+ }}</span>
304
+ </template>
305
+ </i18n-t>
291
306
  </p>
292
307
  <UButton
293
308
  icon="i-lucide-plus"
@@ -296,12 +311,12 @@ function fail(title: string, e: unknown) {
296
311
  class="shrink-0"
297
312
  @click="startCreate"
298
313
  >
299
- New preset
314
+ {{ t('settings.modelConfiguration.list.newPreset') }}
300
315
  </UButton>
301
316
  </div>
302
317
 
303
318
  <p v-if="models.models.length === 0" class="py-4 text-center text-sm text-slate-500">
304
- Loading model catalog…
319
+ {{ t('settings.modelConfiguration.list.loadingCatalog') }}
305
320
  </p>
306
321
 
307
322
  <div v-else class="space-y-3">
@@ -313,7 +328,7 @@ function fail(title: string, e: unknown) {
313
328
  <div class="flex items-center gap-2">
314
329
  <span class="truncate text-sm font-semibold text-slate-100">{{ p.name }}</span>
315
330
  <UBadge v-if="p.isDefault" color="primary" variant="subtle" size="xs">
316
- Default
331
+ {{ t('settings.modelConfiguration.list.default') }}
317
332
  </UBadge>
318
333
  <div class="ml-auto flex items-center gap-1">
319
334
  <UButton
@@ -323,7 +338,7 @@ function fail(title: string, e: unknown) {
323
338
  color="neutral"
324
339
  icon="i-lucide-star"
325
340
  :loading="busy"
326
- title="Set as workspace default"
341
+ :title="t('settings.modelConfiguration.list.setDefaultTitle')"
327
342
  @click="setDefault(p)"
328
343
  />
329
344
  <UButton
@@ -331,7 +346,7 @@ function fail(title: string, e: unknown) {
331
346
  variant="ghost"
332
347
  color="neutral"
333
348
  icon="i-lucide-pencil"
334
- title="Edit preset"
349
+ :title="t('settings.modelConfiguration.list.editTitle')"
335
350
  @click="startEdit(p)"
336
351
  />
337
352
  <UButton
@@ -342,19 +357,26 @@ function fail(title: string, e: unknown) {
342
357
  :disabled="p.isDefault"
343
358
  :loading="busy"
344
359
  :title="
345
- p.isDefault ? 'The default preset cannot be deleted' : 'Delete preset'
360
+ p.isDefault
361
+ ? t('settings.modelConfiguration.list.deleteDisabledTitle')
362
+ : t('settings.modelConfiguration.list.deleteTitle')
346
363
  "
347
364
  @click="remove(p)"
348
365
  />
349
366
  </div>
350
367
  </div>
351
368
  <div class="mt-1.5 text-[11px] text-slate-400">
352
- Base: <span class="text-slate-300">{{ modelLabel(p.baseModelId) }}</span>
369
+ {{ t('settings.modelConfiguration.list.basePrefix') }}
370
+ <span class="text-slate-300">{{ modelLabel(p.baseModelId) }}</span>
353
371
  <span v-if="Object.keys(p.overrides).length">
354
- · {{ Object.keys(p.overrides).length }} override<span
355
- v-if="Object.keys(p.overrides).length !== 1"
356
- >s</span
357
- >
372
+ ·
373
+ {{
374
+ t(
375
+ 'settings.modelConfiguration.list.overrideCount',
376
+ { count: Object.keys(p.overrides).length },
377
+ Object.keys(p.overrides).length,
378
+ )
379
+ }}
358
380
  </span>
359
381
  </div>
360
382
  </div>
@@ -362,7 +384,7 @@ function fail(title: string, e: unknown) {
362
384
  v-if="sortedPresets.length === 0"
363
385
  class="py-6 text-center text-sm text-slate-500"
364
386
  >
365
- No presets yet — create one to map models to your agents.
387
+ {{ t('settings.modelConfiguration.list.empty') }}
366
388
  </p>
367
389
  </div>
368
390
  </template>
@@ -374,11 +396,11 @@ function fail(title: string, e: unknown) {
374
396
  <label
375
397
  class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
376
398
  >
377
- Name
399
+ {{ t('settings.modelConfiguration.editor.nameLabel') }}
378
400
  </label>
379
401
  <UInput
380
402
  v-model="editor.name"
381
- placeholder="e.g. Kimi K2.7"
403
+ :placeholder="t('settings.modelConfiguration.editor.namePlaceholder')"
382
404
  size="sm"
383
405
  class="w-full"
384
406
  />
@@ -388,7 +410,7 @@ function fail(title: string, e: unknown) {
388
410
  <label
389
411
  class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
390
412
  >
391
- Base model (applied to every agent)
413
+ {{ t('settings.modelConfiguration.editor.baseModelLabel') }}
392
414
  </label>
393
415
  <UDropdownMenu
394
416
  :items="baseMenu"
@@ -408,21 +430,21 @@ function fail(title: string, e: unknown) {
408
430
 
409
431
  <label class="flex items-center gap-2 text-sm text-slate-300">
410
432
  <UCheckbox v-model="editor.isDefault" />
411
- Make this the workspace default
433
+ {{ t('settings.modelConfiguration.editor.makeDefault') }}
412
434
  </label>
413
435
  </div>
414
436
 
415
437
  <div>
416
438
  <div class="mb-1 flex items-center justify-between">
417
439
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
418
- Per-agent overrides
440
+ {{ t('settings.modelConfiguration.editor.perAgentOverrides') }}
419
441
  </span>
420
442
  </div>
421
443
  <UInput
422
444
  v-model="filter"
423
445
  icon="i-lucide-search"
424
446
  size="sm"
425
- placeholder="Filter agents…"
447
+ :placeholder="t('settings.modelConfiguration.editor.filterPlaceholder')"
426
448
  class="mb-3 w-full"
427
449
  />
428
450
  <div
@@ -461,17 +483,21 @@ function fail(title: string, e: unknown) {
461
483
  v-if="filteredKinds.length === 0"
462
484
  class="px-4 py-6 text-center text-sm text-slate-500"
463
485
  >
464
- No agents match "{{ filter }}".
486
+ {{ t('settings.modelConfiguration.editor.noAgentsMatch', { filter }) }}
465
487
  </p>
466
488
  </div>
467
489
  </div>
468
490
 
469
491
  <div class="flex items-center justify-end gap-2">
470
492
  <UButton color="neutral" variant="ghost" size="sm" @click="editor = null">
471
- Cancel
493
+ {{ t('common.cancel') }}
472
494
  </UButton>
473
495
  <UButton color="primary" size="sm" :loading="busy" @click="save">
474
- {{ editor.id ? 'Save changes' : 'Create preset' }}
496
+ {{
497
+ editor.id
498
+ ? t('settings.modelConfiguration.editor.saveChanges')
499
+ : t('settings.modelConfiguration.editor.createPreset')
500
+ }}
475
501
  </UButton>
476
502
  </div>
477
503
  </template>
@@ -8,6 +8,7 @@ import { computed, reactive, ref, watch } from 'vue'
8
8
  import type { ObservabilityProviderKind } from '~/types/releaseHealth'
9
9
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
10
10
 
11
+ const { t } = useI18n()
11
12
  const ui = useUiStore()
12
13
  const store = useReleaseHealthStore()
13
14
  const toast = useToast()
@@ -53,7 +54,7 @@ watch(
53
54
  if (site) datadog.site = site
54
55
  await store.loadIncident()
55
56
  } catch (e) {
56
- notifyError('Could not load observability settings', e)
57
+ notifyError(t('settings.observabilityConnection.toast.loadFailed'), e)
57
58
  }
58
59
  },
59
60
  { immediate: true },
@@ -71,16 +72,23 @@ async function saveIncident() {
71
72
  }
72
73
  if (incidentIo.apiKey.trim()) input.incidentIo = { apiKey: incidentIo.apiKey.trim() }
73
74
  if (!input.pagerDuty && !input.incidentIo) {
74
- toast.add({ title: 'Enter PagerDuty or incident.io credentials', color: 'error' })
75
+ toast.add({
76
+ title: t('settings.observabilityConnection.toast.incidentCredsRequired'),
77
+ color: 'error',
78
+ })
75
79
  return
76
80
  }
77
81
  await store.saveIncident(input)
78
82
  pagerDuty.apiToken = ''
79
83
  pagerDuty.fromEmail = ''
80
84
  incidentIo.apiKey = ''
81
- toast.add({ title: 'Incident enrichment saved', icon: 'i-lucide-check', color: 'success' })
85
+ toast.add({
86
+ title: t('settings.observabilityConnection.toast.incidentSaved'),
87
+ icon: 'i-lucide-check',
88
+ color: 'success',
89
+ })
82
90
  } catch (e) {
83
- notifyError('Could not save incident enrichment', e)
91
+ notifyError(t('settings.observabilityConnection.toast.incidentSaveFailed'), e)
84
92
  } finally {
85
93
  incidentBusy.value = false
86
94
  }
@@ -91,7 +99,7 @@ async function disconnectIncident() {
91
99
  try {
92
100
  await store.removeIncident()
93
101
  } catch (e) {
94
- notifyError('Could not disconnect incident enrichment', e)
102
+ notifyError(t('settings.observabilityConnection.toast.incidentDisconnectFailed'), e)
95
103
  } finally {
96
104
  incidentBusy.value = false
97
105
  }
@@ -106,9 +114,13 @@ async function saveConnection() {
106
114
  })
107
115
  datadog.apiKey = ''
108
116
  datadog.appKey = ''
109
- toast.add({ title: 'Observability connected', icon: 'i-lucide-check', color: 'success' })
117
+ toast.add({
118
+ title: t('settings.observabilityConnection.toast.connected'),
119
+ icon: 'i-lucide-check',
120
+ color: 'success',
121
+ })
110
122
  } catch (e) {
111
- notifyError('Could not save the connection', e)
123
+ notifyError(t('settings.observabilityConnection.toast.connectFailed'), e)
112
124
  } finally {
113
125
  busy.value = false
114
126
  }
@@ -119,50 +131,62 @@ async function disconnect() {
119
131
  try {
120
132
  await store.removeConnection()
121
133
  } catch (e) {
122
- notifyError('Could not disconnect', e)
134
+ notifyError(t('settings.observabilityConnection.toast.disconnectFailed'), e)
123
135
  } finally {
124
136
  busy.value = false
125
137
  }
126
138
  }
127
139
 
128
140
  const connectedLabel = computed(() => {
129
- if (!store.connection.connected) return 'Not connected'
141
+ if (!store.connection.connected) return t('settings.observabilityConnection.status.notConnected')
130
142
  const site = store.connection.summary?.site
131
- return site ? `Connected (${site})` : 'Connected'
143
+ return site
144
+ ? t('settings.observabilityConnection.status.connectedWithSite', { site })
145
+ : t('settings.observabilityConnection.status.connected')
132
146
  })
133
147
  </script>
134
148
 
135
149
  <template>
136
- <UModal v-model:open="open" title="Post-release health" :ui="{ content: 'max-w-lg' }">
150
+ <UModal
151
+ v-model:open="open"
152
+ :title="t('settings.observabilityConnection.title')"
153
+ :ui="{ content: 'max-w-lg' }"
154
+ >
137
155
  <template #title>
138
- <IntegrationBackTitle title="Post-release health" @back="back" />
156
+ <IntegrationBackTitle :title="t('settings.observabilityConnection.title')" @back="back" />
139
157
  </template>
140
158
  <template #body>
141
159
  <div class="space-y-4">
142
- <p class="text-sm text-slate-400">
143
- After a release ships, the <code>post-release-health</code> gate watches the configured
144
- observability monitors/SLOs. On a regression it spawns an on-call agent to investigate (a
145
- human decides whether to revert). Map which monitors/SLOs a service watches from that
146
- service's inspector.
147
- </p>
160
+ <i18n-t
161
+ keypath="settings.observabilityConnection.intro"
162
+ tag="p"
163
+ class="text-sm text-slate-400"
164
+ scope="global"
165
+ >
166
+ <template #gate>
167
+ <code>post-release-health</code>
168
+ </template>
169
+ </i18n-t>
148
170
 
149
171
  <section class="space-y-3 rounded-lg border border-slate-700 p-3">
150
172
  <div class="flex items-center justify-between">
151
- <h3 class="text-sm font-semibold">Connection</h3>
173
+ <h3 class="text-sm font-semibold">
174
+ {{ t('settings.observabilityConnection.connection.heading') }}
175
+ </h3>
152
176
  <UBadge :color="store.connection.connected ? 'success' : 'neutral'" variant="soft">
153
177
  {{ connectedLabel }}
154
178
  </UBadge>
155
179
  </div>
156
180
 
157
- <UFormField label="Provider">
181
+ <UFormField :label="t('settings.observabilityConnection.connection.provider')">
158
182
  <USelect v-model="provider" :items="PROVIDERS" value-key="value" class="w-full" />
159
183
  </UFormField>
160
184
 
161
185
  <template v-if="provider === 'datadog'">
162
- <UFormField label="Datadog site">
186
+ <UFormField :label="t('settings.observabilityConnection.datadog.site')">
163
187
  <UInput v-model="datadog.site" placeholder="datadoghq.com" class="w-full" />
164
188
  </UFormField>
165
- <UFormField label="API key">
189
+ <UFormField :label="t('settings.observabilityConnection.datadog.apiKey')">
166
190
  <UInput
167
191
  v-model="datadog.apiKey"
168
192
  type="password"
@@ -170,7 +194,7 @@ const connectedLabel = computed(() => {
170
194
  class="w-full"
171
195
  />
172
196
  </UFormField>
173
- <UFormField label="Application key">
197
+ <UFormField :label="t('settings.observabilityConnection.datadog.appKey')">
174
198
  <UInput
175
199
  v-model="datadog.appKey"
176
200
  type="password"
@@ -186,7 +210,7 @@ const connectedLabel = computed(() => {
186
210
  :disabled="!datadog.apiKey || !datadog.appKey"
187
211
  @click="saveConnection"
188
212
  >
189
- Save connection
213
+ {{ t('settings.observabilityConnection.saveConnection') }}
190
214
  </UButton>
191
215
  <UButton
192
216
  v-if="store.connection.connected"
@@ -195,7 +219,7 @@ const connectedLabel = computed(() => {
195
219
  :loading="busy"
196
220
  @click="disconnect"
197
221
  >
198
- Disconnect
222
+ {{ t('settings.observabilityConnection.disconnect') }}
199
223
  </UButton>
200
224
  </div>
201
225
  </section>
@@ -207,21 +231,25 @@ const connectedLabel = computed(() => {
207
231
  class="space-y-3 rounded-lg border border-slate-700 p-3"
208
232
  >
209
233
  <div class="flex items-center justify-between">
210
- <h3 class="text-sm font-semibold">Incident enrichment</h3>
234
+ <h3 class="text-sm font-semibold">
235
+ {{ t('settings.observabilityConnection.incident.heading') }}
236
+ </h3>
211
237
  <UBadge :color="store.incident.connected ? 'success' : 'neutral'" variant="soft">
212
- {{ store.incident.connected ? 'Configured' : 'Not set' }}
238
+ {{
239
+ store.incident.connected
240
+ ? t('settings.observabilityConnection.incident.configured')
241
+ : t('settings.observabilityConnection.incident.notSet')
242
+ }}
213
243
  </UBadge>
214
244
  </div>
215
245
  <p class="text-[11px] text-slate-400">
216
- Optional. On a regression, the on-call investigation is posted onto an incident these
217
- systems ALREADY opened from the same signals (annotate, never re-alert). Secrets are
218
- write-only — blank leaves a stored value unchanged.
246
+ {{ t('settings.observabilityConnection.incident.description') }}
219
247
  </p>
220
248
 
221
- <UFormField label="PagerDuty API token">
249
+ <UFormField :label="t('settings.observabilityConnection.incident.pagerDutyToken')">
222
250
  <UInput v-model="pagerDuty.apiToken" type="password" class="w-full" />
223
251
  </UFormField>
224
- <UFormField label="PagerDuty From email">
252
+ <UFormField :label="t('settings.observabilityConnection.incident.pagerDutyFromEmail')">
225
253
  <UInput
226
254
  v-model="pagerDuty.fromEmail"
227
255
  type="email"
@@ -229,12 +257,14 @@ const connectedLabel = computed(() => {
229
257
  class="w-full"
230
258
  />
231
259
  </UFormField>
232
- <UFormField label="incident.io API key">
260
+ <UFormField :label="t('settings.observabilityConnection.incident.incidentIoKey')">
233
261
  <UInput v-model="incidentIo.apiKey" type="password" class="w-full" />
234
262
  </UFormField>
235
263
 
236
264
  <div class="flex gap-2">
237
- <UButton :loading="incidentBusy" @click="saveIncident">Save</UButton>
265
+ <UButton :loading="incidentBusy" @click="saveIncident">
266
+ {{ t('common.save') }}
267
+ </UButton>
238
268
  <UButton
239
269
  v-if="store.incident.connected"
240
270
  color="error"
@@ -242,7 +272,7 @@ const connectedLabel = computed(() => {
242
272
  :loading="incidentBusy"
243
273
  @click="disconnectIncident"
244
274
  >
245
- Clear
275
+ {{ t('settings.observabilityConnection.incident.clear') }}
246
276
  </UButton>
247
277
  </div>
248
278
  </section>