@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
@@ -11,6 +11,7 @@ import { computed, ref, watch } from 'vue'
11
11
  import type { OpenRouterModelMeta } from '~/types/openrouter'
12
12
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
13
13
 
14
+ const { t } = useI18n()
14
15
  const ui = useUiStore()
15
16
  const workspace = useWorkspaceStore()
16
17
  const store = useOpenRouterStore()
@@ -89,11 +90,16 @@ const recommendedAvailable = computed(() => {
89
90
 
90
91
  function contextLabel(tokens: number | undefined): string {
91
92
  if (!tokens) return ''
92
- return tokens >= 1000 ? `${Math.round(tokens / 1000)}K ctx` : `${tokens} ctx`
93
+ return tokens >= 1000
94
+ ? t('settings.openRouterCatalog.contextThousands', { value: Math.round(tokens / 1000) })
95
+ : t('settings.openRouterCatalog.context', { value: tokens })
93
96
  }
94
97
 
95
98
  function priceLabel(m: OpenRouterModelMeta): string {
96
- return `${m.inputPerMillion}/${m.outputPerMillion} per Mtok`
99
+ return t('settings.openRouterCatalog.price', {
100
+ input: m.inputPerMillion,
101
+ output: m.outputPerMillion,
102
+ })
97
103
  }
98
104
 
99
105
  function toggle(id: string, on: boolean) {
@@ -132,8 +138,8 @@ async function connectKey() {
132
138
  else await apiKeys.removeUserKey(created.id).catch(() => {})
133
139
  }
134
140
  toast.add({
135
- title: 'Could not connect key',
136
- description: store.refreshError ?? 'OpenRouter rejected the key.',
141
+ title: t('settings.openRouterCatalog.toast.connectFailed'),
142
+ description: store.refreshError ?? t('settings.openRouterCatalog.toast.rejected'),
137
143
  icon: 'i-lucide-triangle-alert',
138
144
  color: 'error',
139
145
  })
@@ -141,10 +147,14 @@ async function connectKey() {
141
147
  }
142
148
  keyValue.value = ''
143
149
  keyLabel.value = ''
144
- toast.add({ title: 'OpenRouter key connected', icon: 'i-lucide-check', color: 'success' })
150
+ toast.add({
151
+ title: t('settings.openRouterCatalog.toast.connected'),
152
+ icon: 'i-lucide-check',
153
+ color: 'success',
154
+ })
145
155
  } catch (e) {
146
156
  toast.add({
147
- title: 'Could not connect key',
157
+ title: t('settings.openRouterCatalog.toast.connectFailed'),
148
158
  description: e instanceof Error ? e.message : String(e),
149
159
  icon: 'i-lucide-triangle-alert',
150
160
  color: 'error',
@@ -159,8 +169,8 @@ async function refresh() {
159
169
  const result = await store.refresh(workspace.workspaceId)
160
170
  if (!result.reachable) {
161
171
  toast.add({
162
- title: 'Could not reach OpenRouter',
163
- description: store.refreshError ?? 'Connect an OpenRouter key first.',
172
+ title: t('settings.openRouterCatalog.toast.unreachable'),
173
+ description: store.refreshError ?? t('settings.openRouterCatalog.toast.connectFirst'),
164
174
  icon: 'i-lucide-triangle-alert',
165
175
  color: 'error',
166
176
  })
@@ -179,10 +189,14 @@ async function save() {
179
189
  await store.save(workspace.workspaceId, models2)
180
190
  // Reflect newly-enabled models in the picker immediately.
181
191
  await models.refresh(workspace.workspaceId)
182
- toast.add({ title: 'OpenRouter catalog saved', icon: 'i-lucide-check', color: 'success' })
192
+ toast.add({
193
+ title: t('settings.openRouterCatalog.toast.saved'),
194
+ icon: 'i-lucide-check',
195
+ color: 'success',
196
+ })
183
197
  } catch (e) {
184
198
  toast.add({
185
- title: 'Could not save catalog',
199
+ title: t('settings.openRouterCatalog.toast.saveFailed'),
186
200
  description: e instanceof Error ? e.message : String(e),
187
201
  icon: 'i-lucide-triangle-alert',
188
202
  color: 'error',
@@ -206,9 +220,11 @@ function manageKeys() {
206
220
  <template #body>
207
221
  <div class="space-y-4">
208
222
  <p class="text-xs text-slate-400">
209
- Reach <strong>300+ models</strong> through one gateway. Add your OpenRouter key below,
210
- then enable the models you want — they appear in the model picker with their context
211
- window and price, and meter against your spend budget.
223
+ <i18n-t keypath="settings.openRouterCatalog.intro" tag="span" scope="global">
224
+ <template #models>
225
+ <strong>{{ t('settings.openRouterCatalog.introModels') }}</strong>
226
+ </template>
227
+ </i18n-t>
212
228
  </p>
213
229
 
214
230
  <!-- Step 1: connect a key (inline) — hidden once a key is connected -->
@@ -217,40 +233,49 @@ function manageKeys() {
217
233
  class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/60 p-4"
218
234
  >
219
235
  <h4 class="text-xs font-semibold uppercase tracking-wide text-slate-500">
220
- Connect your OpenRouter key
236
+ {{ t('settings.openRouterCatalog.connectHeading') }}
221
237
  </h4>
222
238
  <ol class="list-decimal space-y-1 pl-5 text-sm text-slate-300">
223
239
  <li>
224
- Open
225
- <a
226
- href="https://openrouter.ai/keys"
227
- target="_blank"
228
- rel="noopener noreferrer"
229
- class="text-primary-400 underline"
230
- >openrouter.ai → Keys ↗</a
231
- >
232
- and create an API key.
240
+ <i18n-t keypath="settings.openRouterCatalog.step1" tag="span" scope="global">
241
+ <template #link>
242
+ <a
243
+ href="https://openrouter.ai/keys"
244
+ target="_blank"
245
+ rel="noopener noreferrer"
246
+ class="text-primary-400 underline"
247
+ >{{ t('settings.openRouterCatalog.step1Link') }}</a
248
+ >
249
+ </template>
250
+ </i18n-t>
233
251
  </li>
234
252
  <li>
235
- Copy the key (starts with <span class="font-mono">sk-or-…</span>) and paste it below.
253
+ <i18n-t keypath="settings.openRouterCatalog.step2" tag="span" scope="global">
254
+ <template #prefix>
255
+ <span class="font-mono">sk-or-…</span>
256
+ </template>
257
+ </i18n-t>
236
258
  </li>
237
259
  </ol>
238
260
  <div class="flex flex-wrap items-end gap-3">
239
- <UFormField label="Scope">
261
+ <UFormField :label="t('settings.openRouterCatalog.scope')">
240
262
  <USelect
241
263
  v-model="keyScope"
242
264
  :items="[
243
- { label: 'This workspace', value: 'workspace' },
244
- { label: 'My keys (only me)', value: 'user' },
265
+ { label: t('settings.openRouterCatalog.scopeWorkspace'), value: 'workspace' },
266
+ { label: t('settings.openRouterCatalog.scopeUser'), value: 'user' },
245
267
  ]"
246
268
  class="w-48"
247
269
  />
248
270
  </UFormField>
249
- <UFormField label="Label (optional)" class="flex-1">
250
- <UInput v-model="keyLabel" placeholder="e.g. team key" />
271
+ <UFormField :label="t('settings.openRouterCatalog.labelOptional')" class="flex-1">
272
+ <UInput
273
+ v-model="keyLabel"
274
+ :placeholder="t('settings.openRouterCatalog.labelPlaceholder')"
275
+ />
251
276
  </UFormField>
252
277
  </div>
253
- <UFormField label="API key">
278
+ <UFormField :label="t('settings.openRouterCatalog.apiKey')">
254
279
  <UTextarea
255
280
  v-model="keyValue"
256
281
  :rows="2"
@@ -265,7 +290,7 @@ function manageKeys() {
265
290
  icon="i-lucide-plus"
266
291
  @click="connectKey()"
267
292
  >
268
- Connect & browse
293
+ {{ t('settings.openRouterCatalog.connectAndBrowse') }}
269
294
  </UButton>
270
295
  </div>
271
296
  </div>
@@ -277,10 +302,10 @@ function manageKeys() {
277
302
  >
278
303
  <span class="flex items-center gap-2 text-slate-300">
279
304
  <UIcon name="i-lucide-check-circle" class="h-4 w-4 text-emerald-400" />
280
- OpenRouter key connected
305
+ {{ t('settings.openRouterCatalog.keyConnected') }}
281
306
  </span>
282
307
  <UButton color="neutral" variant="ghost" size="xs" @click="manageKeys()">
283
- Manage in Vendors & keys
308
+ {{ t('settings.openRouterCatalog.manageKeys') }}
284
309
  </UButton>
285
310
  </div>
286
311
 
@@ -295,7 +320,7 @@ function manageKeys() {
295
320
  :loading="store.refreshing"
296
321
  @click="refresh()"
297
322
  >
298
- Refresh catalog
323
+ {{ t('settings.openRouterCatalog.refresh') }}
299
324
  </UButton>
300
325
  <UButton
301
326
  v-if="recommendedAvailable.length"
@@ -305,14 +330,14 @@ function manageKeys() {
305
330
  icon="i-lucide-sparkles"
306
331
  @click="enableRecommended()"
307
332
  >
308
- Enable recommended
333
+ {{ t('settings.openRouterCatalog.enableRecommended') }}
309
334
  </UButton>
310
335
  <UInput
311
336
  v-model="filter"
312
337
  size="sm"
313
338
  class="flex-1"
314
339
  icon="i-lucide-search"
315
- placeholder="Filter by name or slug…"
340
+ :placeholder="t('settings.openRouterCatalog.filterPlaceholder')"
316
341
  />
317
342
  </div>
318
343
 
@@ -341,12 +366,17 @@ function manageKeys() {
341
366
  </label>
342
367
  </div>
343
368
  <p v-else class="text-xs text-slate-500">
344
- No models yet — hit <span class="text-slate-300">Refresh catalog</span> to load
345
- OpenRouter's live list.
369
+ <i18n-t keypath="settings.openRouterCatalog.empty" tag="span" scope="global">
370
+ <template #action>
371
+ <span class="text-slate-300">{{ t('settings.openRouterCatalog.refresh') }}</span>
372
+ </template>
373
+ </i18n-t>
346
374
  </p>
347
375
 
348
376
  <div class="flex items-center justify-between">
349
- <span class="text-xs text-slate-500">{{ selectedCount }} enabled</span>
377
+ <span class="text-xs text-slate-500">{{
378
+ t('settings.openRouterCatalog.enabledCount', { count: selectedCount }, selectedCount)
379
+ }}</span>
350
380
  <UButton
351
381
  color="primary"
352
382
  variant="soft"
@@ -355,7 +385,7 @@ function manageKeys() {
355
385
  :loading="busy"
356
386
  @click="save()"
357
387
  >
358
- Save
388
+ {{ t('common.save') }}
359
389
  </UButton>
360
390
  </div>
361
391
  </template>
@@ -12,23 +12,28 @@ import type { ProviderConnectionKind } from '~/types/providerConnections'
12
12
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
13
13
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
14
14
 
15
+ const { t } = useI18n()
15
16
  const ui = useUiStore()
16
17
  const store = useProviderConnectionsStore()
17
18
  const toast = useToast()
18
19
 
19
- const META: Record<ProviderConnectionKind, { title: string; icon: string; blurb: string }> = {
20
- environment: {
21
- title: 'Ephemeral environment provider',
22
- icon: 'i-lucide-cloud',
23
- blurb:
24
- 'Where the Tester agent runs against a live preview environment. Configure the per-workspace settings and credentials your provider needs.',
25
- },
26
- 'runner-pool': {
27
- title: 'Self-hosted runner pool',
28
- icon: 'i-lucide-server-cog',
29
- blurb:
30
- 'Where the coding agents run when not using Cloudflare Containers. Configure the pool scheduler endpoint and credentials.',
31
- },
20
+ // Per-kind icon (display-only). The title + blurb copy resolve through the i18n catalog
21
+ // via literal keys keyed off the provider-connection kind (see TITLE_KEYS / BLURB_KEYS).
22
+ const ICONS: Record<ProviderConnectionKind, string> = {
23
+ environment: 'i-lucide-cloud',
24
+ 'runner-pool': 'i-lucide-server-cog',
25
+ }
26
+
27
+ // Exhaustive Record keyed off the provider-connection-kind union (a missing member fails
28
+ // the typecheck); each value is a LITERAL catalog key so the typed-message-keys check sees
29
+ // it. Leaf keys mirror the kind verbatim.
30
+ const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
31
+ environment: 'settings.providerConnection.kind.environment.title',
32
+ 'runner-pool': 'settings.providerConnection.kind.runner-pool.title',
33
+ }
34
+ const BLURB_KEYS: Record<ProviderConnectionKind, string> = {
35
+ environment: 'settings.providerConnection.kind.environment.blurb',
36
+ 'runner-pool': 'settings.providerConnection.kind.runner-pool.blurb',
32
37
  }
33
38
 
34
39
  const kind = computed<ProviderConnectionKind | null>(() => ui.providerConnectionKind)
@@ -40,7 +45,15 @@ const open = computed({
40
45
  })
41
46
  const back = useIntegrationBack(open)
42
47
 
43
- const meta = computed(() => (kind.value ? META[kind.value] : null))
48
+ const meta = computed(() =>
49
+ kind.value
50
+ ? {
51
+ title: t(TITLE_KEYS[kind.value]),
52
+ icon: ICONS[kind.value],
53
+ blurb: t(BLURB_KEYS[kind.value]),
54
+ }
55
+ : null,
56
+ )
44
57
  const descriptor = computed(() => (kind.value ? store.descriptorFor(kind.value) : null))
45
58
  const connection = computed(() => (kind.value ? store.connectionFor(kind.value) : null))
46
59
 
@@ -69,7 +82,7 @@ async function setDelegation(patch: {
69
82
  try {
70
83
  await settings.update(patch)
71
84
  } catch (e) {
72
- notifyError('Could not update delegation', e)
85
+ notifyError(t('settings.providerConnection.delegation.updateFailed'), e)
73
86
  } finally {
74
87
  savingDelegation.value = false
75
88
  }
@@ -226,9 +239,13 @@ async function save() {
226
239
  await store.updateSecrets(kind.value, buildSecretsOnly())
227
240
  }
228
241
  resetDraft()
229
- toast.add({ title: `${meta.value?.title} saved`, icon: 'i-lucide-check', color: 'success' })
242
+ toast.add({
243
+ title: t('settings.providerConnection.toast.saved', { title: meta.value?.title ?? '' }),
244
+ icon: 'i-lucide-check',
245
+ color: 'success',
246
+ })
230
247
  } catch (e) {
231
- notifyError('Could not save the connection', e)
248
+ notifyError(t('settings.providerConnection.toast.saveFailed'), e)
232
249
  } finally {
233
250
  busy.value = false
234
251
  }
@@ -240,9 +257,9 @@ async function remove() {
240
257
  try {
241
258
  await store.remove(kind.value)
242
259
  resetDraft()
243
- toast.add({ title: 'Connection removed', icon: 'i-lucide-check' })
260
+ toast.add({ title: t('settings.providerConnection.toast.removed'), icon: 'i-lucide-check' })
244
261
  } catch (e) {
245
- notifyError('Could not remove the connection', e)
262
+ notifyError(t('settings.providerConnection.toast.removeFailed'), e)
246
263
  } finally {
247
264
  busy.value = false
248
265
  }
@@ -254,16 +271,24 @@ function fieldHelp(key: string): string | undefined {
254
271
  if (!f) return undefined
255
272
  const filled = (values.value[key] ?? '').trim()
256
273
  if (f.default !== undefined && !filled) {
257
- return f.help ? `${f.help} · Defaults to ${f.default}` : `Defaults to ${f.default}`
274
+ const defaulted = t('settings.providerConnection.field.defaultsTo', { value: f.default })
275
+ return f.help ? `${f.help} · ${defaulted}` : defaulted
258
276
  }
259
277
  return f.help
260
278
  }
261
279
  </script>
262
280
 
263
281
  <template>
264
- <UModal v-model:open="open" :title="meta?.title ?? 'Provider'" :ui="{ content: 'max-w-xl' }">
282
+ <UModal
283
+ v-model:open="open"
284
+ :title="meta?.title ?? t('settings.providerConnection.fallbackTitle')"
285
+ :ui="{ content: 'max-w-xl' }"
286
+ >
265
287
  <template #title>
266
- <IntegrationBackTitle :title="meta?.title ?? 'Provider'" @back="back" />
288
+ <IntegrationBackTitle
289
+ :title="meta?.title ?? t('settings.providerConnection.fallbackTitle')"
290
+ @back="back"
291
+ />
267
292
  </template>
268
293
  <template #body>
269
294
  <!-- Local-mode infrastructure delegation: the local-vs-external choice for BOTH
@@ -273,11 +298,11 @@ function fieldHelp(key: string): string | undefined {
273
298
  class="mb-4 space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
274
299
  >
275
300
  <div>
276
- <h3 class="text-sm font-semibold text-slate-200">Local delegation</h3>
301
+ <h3 class="text-sm font-semibold text-slate-200">
302
+ {{ t('settings.providerConnection.delegation.title') }}
303
+ </h3>
277
304
  <p class="mt-1 text-[11px] text-slate-400">
278
- By default this machine runs everything locally — container agents on host Docker, the
279
- Tester's infrastructure via in-container docker-compose. Opt in below to delegate either
280
- concern to an external service instead. Applies only in local mode.
305
+ {{ t('settings.providerConnection.delegation.intro') }}
281
306
  </p>
282
307
  </div>
283
308
 
@@ -290,20 +315,28 @@ function fieldHelp(key: string): string | undefined {
290
315
  :disabled="savingDelegation || !runnerPoolRegistered"
291
316
  @update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
292
317
  />
293
- <span class="text-sm text-slate-200">Run container agents on the runner pool</span>
318
+ <span class="text-sm text-slate-200">
319
+ {{ t('settings.providerConnection.delegation.agentsToggle') }}
320
+ </span>
294
321
  </label>
295
322
  <p class="pl-9 text-[11px] text-slate-400">
296
- Dispatch every container agent (coder, tester, merger, bootstrap, …) to this workspace's
297
- self-hosted runner pool instead of host Docker.
323
+ {{ t('settings.providerConnection.delegation.agentsHint') }}
298
324
  <template v-if="!runnerPoolRegistered">
299
- <button
300
- type="button"
301
- class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
302
- @click="openRunnerPoolPanel"
325
+ <i18n-t
326
+ keypath="settings.providerConnection.delegation.registerPoolPrompt"
327
+ tag="span"
328
+ scope="global"
303
329
  >
304
- Register a runner pool
305
- </button>
306
- first to enable this.
330
+ <template #link>
331
+ <button
332
+ type="button"
333
+ class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
334
+ @click="openRunnerPoolPanel"
335
+ >
336
+ {{ t('settings.providerConnection.delegation.registerPoolLink') }}
337
+ </button>
338
+ </template>
339
+ </i18n-t>
307
340
  </template>
308
341
  </p>
309
342
  </div>
@@ -318,12 +351,11 @@ function fieldHelp(key: string): string | undefined {
318
351
  @update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
319
352
  />
320
353
  <span class="text-sm text-slate-200">
321
- Provision Tester environments via the provider
354
+ {{ t('settings.providerConnection.delegation.envToggle') }}
322
355
  </span>
323
356
  </label>
324
357
  <p class="pl-9 text-[11px] text-slate-400">
325
- Stand the Tester's preview environment up through the environment provider configured
326
- below instead of in-container docker-compose. Connect a provider first to enable this.
358
+ {{ t('settings.providerConnection.delegation.envHint') }}
327
359
  </p>
328
360
  </div>
329
361
  </section>
@@ -334,15 +366,17 @@ function fieldHelp(key: string): string | undefined {
334
366
  v-if="isLocal && kind === 'runner-pool'"
335
367
  class="mb-4 rounded-md border border-slate-700 bg-slate-900/40 px-3 py-2 text-[11px] text-slate-400"
336
368
  >
337
- Register your pool here, then enable "Run container agents on the runner pool" on the
338
- <button
339
- type="button"
340
- class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
341
- @click="ui.openProviderConnection('environment')"
342
- >
343
- Ephemeral environments
344
- </button>
345
- screen to route this workspace's agents to it.
369
+ <i18n-t keypath="settings.providerConnection.runnerPoolLocalHint" tag="span" scope="global">
370
+ <template #link>
371
+ <button
372
+ type="button"
373
+ class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
374
+ @click="ui.openProviderConnection('environment')"
375
+ >
376
+ {{ t('settings.providerConnection.ephemeralEnvironments') }}
377
+ </button>
378
+ </template>
379
+ </i18n-t>
346
380
  </p>
347
381
 
348
382
  <div v-if="descriptor" class="space-y-4">
@@ -355,7 +389,11 @@ function fieldHelp(key: string): string | undefined {
355
389
  class="shrink-0"
356
390
  @click="showLogs = !showLogs"
357
391
  >
358
- {{ showLogs ? 'Hide logs' : 'View logs' }}
392
+ {{
393
+ showLogs
394
+ ? t('settings.providerConnection.hideLogs')
395
+ : t('settings.providerConnection.viewLogs')
396
+ }}
359
397
  </UButton>
360
398
  </div>
361
399
 
@@ -369,7 +407,9 @@ function fieldHelp(key: string): string | undefined {
369
407
  >
370
408
  <div>
371
409
  <span class="font-medium text-slate-200">{{ connection.label }}</span>
372
- <div class="text-[11px] text-emerald-400">Connected · {{ connection.baseUrl }}</div>
410
+ <div class="text-[11px] text-emerald-400">
411
+ {{ t('settings.providerConnection.connectedAt', { baseUrl: connection.baseUrl }) }}
412
+ </div>
373
413
  </div>
374
414
  <UButton
375
415
  icon="i-lucide-trash-2"
@@ -386,7 +426,11 @@ function fieldHelp(key: string): string | undefined {
386
426
  v-if="descriptor.missingRequired.length"
387
427
  class="rounded-md border border-amber-500/40 bg-amber-950/40 px-3 py-2 text-xs text-amber-200"
388
428
  >
389
- Missing required config: {{ descriptor.missingRequired.join(', ') }}
429
+ {{
430
+ t('settings.providerConnection.missingConfig', {
431
+ fields: descriptor.missingRequired.join(', '),
432
+ })
433
+ }}
390
434
  </div>
391
435
 
392
436
  <!-- Generic, descriptor-driven field form -->
@@ -395,7 +439,11 @@ function fieldHelp(key: string): string | undefined {
395
439
  class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3"
396
440
  >
397
441
  <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
398
- {{ connection ? 'Update configuration' : 'Connect' }}
442
+ {{
443
+ connection
444
+ ? t('settings.providerConnection.form.updateConfiguration')
445
+ : t('settings.providerConnection.form.connect')
446
+ }}
399
447
  </p>
400
448
  <!-- A native re-register replaces the whole manifest; secrets are write-only so they
401
449
  must be re-supplied. Non-secret config is prefilled, so it survives a save. -->
@@ -403,15 +451,22 @@ function fieldHelp(key: string): string | undefined {
403
451
  v-if="connection && canAuthor && hasSecretFields"
404
452
  class="text-[11px] text-amber-300/80"
405
453
  >
406
- Re-enter the secret field{{ secretFieldCount > 1 ? 's' : '' }} to save changes — stored
407
- secrets are write-only and aren't shown.
454
+ {{
455
+ t(
456
+ 'settings.providerConnection.form.reenterSecrets',
457
+ { count: secretFieldCount },
458
+ secretFieldCount,
459
+ )
460
+ }}
408
461
  </p>
409
462
 
410
463
  <UFormField
411
464
  v-for="field in descriptor.configFields"
412
465
  :key="field.key"
413
466
  :label="
414
- field.label + (field.required && field.default === undefined ? '' : ' (optional)')
467
+ field.required && field.default === undefined
468
+ ? field.label
469
+ : t('settings.providerConnection.form.optionalLabel', { label: field.label })
415
470
  "
416
471
  :help="fieldHelp(field.key)"
417
472
  >
@@ -439,19 +494,19 @@ function fieldHelp(key: string): string | undefined {
439
494
  :loading="testing"
440
495
  @click="test()"
441
496
  >
442
- Test connection
497
+ {{ t('settings.providerConnection.test.button') }}
443
498
  </UButton>
444
499
  <span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
445
- {{ testResult.message ?? 'Connection OK' }}
500
+ {{ testResult.message ?? t('settings.providerConnection.test.ok') }}
446
501
  </span>
447
502
  <span v-else-if="testResult" class="text-xs text-rose-400">
448
- {{ testResult.message ?? 'Connection failed' }}
503
+ {{ testResult.message ?? t('settings.providerConnection.test.failed') }}
449
504
  </span>
450
505
  </div>
451
506
 
452
507
  <div class="flex justify-end">
453
508
  <UButton color="primary" size="sm" :loading="busy" :disabled="!canSave" @click="save()">
454
- {{ connection ? 'Save' : 'Connect' }}
509
+ {{ connection ? t('common.save') : t('settings.providerConnection.form.connect') }}
455
510
  </UButton>
456
511
  </div>
457
512
  </div>
@@ -461,8 +516,7 @@ function fieldHelp(key: string): string | undefined {
461
516
  v-else
462
517
  class="rounded-md border border-slate-700 bg-slate-900/40 px-3 py-3 text-xs text-slate-400"
463
518
  >
464
- This provider is configured by authoring a manifest. The in-app manifest editor isn't
465
- available yet — register it via the API for now.
519
+ {{ t('settings.providerConnection.manifestEditorUnavailable') }}
466
520
  </div>
467
521
  </div>
468
522
  </template>
@@ -6,6 +6,7 @@
6
6
  // serviceFragmentDefaults store (the backend replaces the whole list on each change).
7
7
  import { onMounted, ref } from 'vue'
8
8
 
9
+ const { t } = useI18n()
9
10
  const fragments = useFragmentsStore()
10
11
  const defaults = useServiceFragmentDefaultsStore()
11
12
  const ui = useUiStore()
@@ -41,7 +42,7 @@ async function save(ids: string[]) {
41
42
  await defaults.set(ids)
42
43
  } catch (e) {
43
44
  toast.add({
44
- title: 'Could not save default fragments',
45
+ title: t('settings.serviceFragmentDefaults.saveFailed'),
45
46
  description: e instanceof Error ? e.message : String(e),
46
47
  icon: 'i-lucide-triangle-alert',
47
48
  color: 'error',
@@ -64,16 +65,12 @@ function remove(id: string) {
64
65
  <template>
65
66
  <div class="space-y-4">
66
67
  <p class="text-xs text-slate-400">
67
- Pick the best-practice fragments every <span class="text-slate-300">new</span> service starts
68
- with. Their guidance is folded into the prompt of every
69
- <span class="text-slate-300">code-aware</span> agent (coder, reviewer, architect, fixers) on
70
- the service's tasks. You can refine the set per service in its inspector; changing this
71
- default does not affect services that already exist.
68
+ {{ t('settings.serviceFragmentDefaults.intro') }}
72
69
  </p>
73
70
 
74
71
  <div class="flex items-center justify-between">
75
72
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
76
- Default fragments
73
+ {{ t('settings.serviceFragmentDefaults.defaultFragments') }}
77
74
  </span>
78
75
  <UDropdownMenu v-if="menu.length" :items="menu" :ui="{ content: 'max-h-72 overflow-y-auto' }">
79
76
  <UButton
@@ -84,7 +81,7 @@ function remove(id: string) {
84
81
  trailing-icon="i-lucide-chevron-down"
85
82
  :loading="busy"
86
83
  >
87
- Add fragment
84
+ {{ t('settings.serviceFragmentDefaults.addFragment') }}
88
85
  </UButton>
89
86
  </UDropdownMenu>
90
87
  </div>
@@ -104,26 +101,26 @@ function remove(id: string) {
104
101
  </UBadge>
105
102
  </div>
106
103
  <p v-else class="text-[11px] text-slate-500">
107
- None new services start with no service-level fragments.
104
+ {{ t('settings.serviceFragmentDefaults.empty') }}
108
105
  </p>
109
106
 
110
107
  <div class="flex flex-wrap gap-x-4 gap-y-1 border-t border-slate-800 pt-3 text-[11px]">
111
108
  <span class="text-slate-500">
112
- Need to add or edit the fragments themselves (hand-authored, linked docs, repos)?
109
+ {{ t('settings.serviceFragmentDefaults.footer.question') }}
113
110
  </span>
114
111
  <button
115
112
  type="button"
116
113
  class="font-medium text-primary-400 hover:underline"
117
114
  @click="ui.openFragmentLibrary()"
118
115
  >
119
- Manage this board's fragment library →
116
+ {{ t('settings.serviceFragmentDefaults.footer.manageBoard') }}
120
117
  </button>
121
118
  <button
122
119
  type="button"
123
120
  class="font-medium text-primary-400 hover:underline"
124
121
  @click="ui.openAccountSettings('fragments')"
125
122
  >
126
- Manage account fragments →
123
+ {{ t('settings.serviceFragmentDefaults.footer.manageAccount') }}
127
124
  </button>
128
125
  </div>
129
126
  </div>