@cat-factory/app 0.46.10 → 0.46.12

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 (48) hide show
  1. package/app/components/auth/AuthGate.vue +2 -1
  2. package/app/components/auth/LoginScreen.vue +41 -31
  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 +75 -32
  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/observability/StepMetricsBar.vue +31 -11
  20. package/app/components/observability/StepModelActivity.vue +3 -2
  21. package/app/components/panels/AgentStepDetail.vue +52 -39
  22. package/app/components/panels/DecisionModal.vue +12 -5
  23. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  24. package/app/components/panels/InspectorPanel.vue +35 -28
  25. package/app/components/panels/ObservabilityPanel.vue +92 -49
  26. package/app/components/panels/StepMetadataCard.vue +87 -30
  27. package/app/components/panels/StepRestartControl.vue +4 -3
  28. package/app/components/panels/StepRunMeta.vue +23 -10
  29. package/app/components/panels/StepTestReport.vue +14 -11
  30. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  31. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  32. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  33. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  34. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  35. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  36. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  37. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  38. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  39. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  40. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  41. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  42. package/app/components/providers/PersonalSubscriptionSection.vue +1 -2
  43. package/i18n/locales/en.json +814 -0
  44. package/i18n/locales/es.json +811 -0
  45. package/i18n/locales/fr.json +811 -0
  46. package/i18n/locales/pl.json +811 -0
  47. package/i18n/locales/uk.json +811 -0
  48. package/package.json +1 -1
@@ -7,6 +7,7 @@ import LoginScreen from '~/components/auth/LoginScreen.vue'
7
7
  // inside the default slot, so it only fires once the user is allowed in.
8
8
  const auth = useAuthStore()
9
9
  const route = useRoute()
10
+ const { t } = useI18n()
10
11
 
11
12
  // The password-reset page is public: a recipient of an emailed reset link is signed
12
13
  // out, so it must render even when auth is required and there's no user.
@@ -21,7 +22,7 @@ onMounted(() => auth.bootstrap())
21
22
  class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-slate-950 text-slate-400"
22
23
  >
23
24
  <UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
24
- <span class="text-sm">Loading…</span>
25
+ <span class="text-sm">{{ t('auth.gate.loading') }}</span>
25
26
  </div>
26
27
 
27
28
  <slot v-else-if="isPublicRoute" />
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
3
3
  import { apiErrorEnvelope } from '~/composables/api/errors'
4
4
 
5
5
  const auth = useAuthStore()
6
+ const { t } = useI18n()
6
7
 
7
8
  // An invite token may ride in on the URL (?invite=…) — it flows through the OAuth
8
9
  // redirect and the password signup so a brand-new user can join the org on first login.
@@ -41,8 +42,7 @@ async function submitPassword() {
41
42
  // Reload so the app boots with the new session.
42
43
  if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
43
44
  } catch (e) {
44
- error.value =
45
- apiErrorEnvelope(e)?.message ?? 'Sign-in failed. Check your details and try again.'
45
+ error.value = apiErrorEnvelope(e)?.message ?? t('auth.login.signInFailed')
46
46
  } finally {
47
47
  busy.value = false
48
48
  }
@@ -56,7 +56,7 @@ async function submitForgot() {
56
56
  forgotSent.value = true
57
57
  } catch {
58
58
  // The request endpoint is generic by design; only an infra error lands here.
59
- error.value = 'Something went wrong. Please try again.'
59
+ error.value = t('auth.login.genericError')
60
60
  } finally {
61
61
  busy.value = false
62
62
  }
@@ -81,11 +81,11 @@ const showOAuthDivider = computed(
81
81
  >
82
82
  <div class="mb-6 text-center">
83
83
  <UIcon name="i-lucide-layout-dashboard" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
84
- <h1 class="mb-1 text-lg font-semibold text-white">Architecture Board</h1>
84
+ <h1 class="mb-1 text-lg font-semibold text-white">{{ t('auth.login.appTitle') }}</h1>
85
85
  <p class="text-sm text-slate-400">
86
- <template v-if="mode === 'forgot'">Reset your password.</template>
86
+ <template v-if="mode === 'forgot'">{{ t('auth.login.forgotSubtitle') }}</template>
87
87
  <template v-else>{{
88
- invite ? 'Accept your invitation to continue.' : 'Sign in to continue.'
88
+ invite ? t('auth.login.inviteSubtitle') : t('auth.login.subtitle')
89
89
  }}</template>
90
90
  </p>
91
91
  </div>
@@ -100,7 +100,7 @@ const showOAuthDivider = computed(
100
100
  icon="i-lucide-github"
101
101
  @click="auth.login(invite)"
102
102
  >
103
- Continue with GitHub
103
+ {{ t('auth.login.continueWithGithub') }}
104
104
  </UButton>
105
105
  <UButton
106
106
  v-if="auth.providers.google"
@@ -111,7 +111,7 @@ const showOAuthDivider = computed(
111
111
  icon="i-lucide-mail"
112
112
  @click="auth.loginWithGoogle(invite)"
113
113
  >
114
- Continue with Google
114
+ {{ t('auth.login.continueWithGoogle') }}
115
115
  </UButton>
116
116
  </div>
117
117
 
@@ -119,7 +119,8 @@ const showOAuthDivider = computed(
119
119
  v-if="showOAuthDivider && mode !== 'forgot'"
120
120
  class="my-4 flex items-center gap-3 text-xs text-slate-500"
121
121
  >
122
- <span class="h-px flex-1 bg-slate-800" /> or <span class="h-px flex-1 bg-slate-800" />
122
+ <span class="h-px flex-1 bg-slate-800" /> {{ t('auth.login.or') }}
123
+ <span class="h-px flex-1 bg-slate-800" />
123
124
  </div>
124
125
 
125
126
  <!-- Email / password -->
@@ -131,7 +132,7 @@ const showOAuthDivider = computed(
131
132
  <UInput
132
133
  v-if="mode === 'signup'"
133
134
  v-model="name"
134
- placeholder="Name (optional)"
135
+ :placeholder="t('auth.login.namePlaceholder')"
135
136
  icon="i-lucide-user"
136
137
  size="lg"
137
138
  class="w-full"
@@ -140,7 +141,7 @@ const showOAuthDivider = computed(
140
141
  v-model="email"
141
142
  type="email"
142
143
  required
143
- placeholder="Email"
144
+ :placeholder="t('auth.login.emailPlaceholder')"
144
145
  icon="i-lucide-at-sign"
145
146
  size="lg"
146
147
  class="w-full"
@@ -149,36 +150,46 @@ const showOAuthDivider = computed(
149
150
  v-model="password"
150
151
  type="password"
151
152
  required
152
- placeholder="Password"
153
+ :placeholder="t('auth.login.passwordPlaceholder')"
153
154
  icon="i-lucide-lock"
154
155
  size="lg"
155
156
  class="w-full"
156
157
  />
157
158
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
158
159
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
159
- {{ mode === 'signup' ? 'Create account' : 'Sign in' }}
160
+ {{ mode === 'signup' ? t('auth.login.createAccount') : t('auth.login.signIn') }}
160
161
  </UButton>
161
162
  <p class="text-center text-xs text-slate-400">
162
163
  <template v-if="mode === 'login'">
163
- Need an account?
164
- <button
165
- type="button"
166
- class="text-indigo-400 hover:underline"
167
- @click="setMode('signup')"
168
- >
169
- Sign up
170
- </button>
164
+ <i18n-t keypath="auth.login.needAccount" tag="span" scope="global">
165
+ <template #signUp>
166
+ <button
167
+ type="button"
168
+ class="text-indigo-400 hover:underline"
169
+ @click="setMode('signup')"
170
+ >
171
+ {{ t('auth.login.signUp') }}
172
+ </button>
173
+ </template>
174
+ </i18n-t>
171
175
  </template>
172
176
  <template v-else>
173
- Already have an account?
174
- <button type="button" class="text-indigo-400 hover:underline" @click="setMode('login')">
175
- Sign in
176
- </button>
177
+ <i18n-t keypath="auth.login.haveAccount" tag="span" scope="global">
178
+ <template #signIn>
179
+ <button
180
+ type="button"
181
+ class="text-indigo-400 hover:underline"
182
+ @click="setMode('login')"
183
+ >
184
+ {{ t('auth.login.signIn') }}
185
+ </button>
186
+ </template>
187
+ </i18n-t>
177
188
  </template>
178
189
  </p>
179
190
  <p v-if="mode === 'login'" class="text-center text-xs text-slate-400">
180
191
  <button type="button" class="text-indigo-400 hover:underline" @click="setMode('forgot')">
181
- Forgot password?
192
+ {{ t('auth.login.forgotPassword') }}
182
193
  </button>
183
194
  </p>
184
195
  </form>
@@ -191,8 +202,7 @@ const showOAuthDivider = computed(
191
202
  >
192
203
  <template v-if="forgotSent">
193
204
  <p class="text-sm text-slate-300">
194
- If an account exists for that email, a password reset link is on its way. Check your
195
- inbox.
205
+ {{ t('auth.login.forgotSent') }}
196
206
  </p>
197
207
  </template>
198
208
  <template v-else>
@@ -200,19 +210,19 @@ const showOAuthDivider = computed(
200
210
  v-model="email"
201
211
  type="email"
202
212
  required
203
- placeholder="Email"
213
+ :placeholder="t('auth.login.emailPlaceholder')"
204
214
  icon="i-lucide-at-sign"
205
215
  size="lg"
206
216
  class="w-full"
207
217
  />
208
218
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
209
219
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
210
- Send reset link
220
+ {{ t('auth.login.sendResetLink') }}
211
221
  </UButton>
212
222
  </template>
213
223
  <p class="text-center text-xs text-slate-400">
214
224
  <button type="button" class="text-indigo-400 hover:underline" @click="setMode('login')">
215
- Back to sign in
225
+ {{ t('auth.login.backToSignIn') }}
216
226
  </button>
217
227
  </p>
218
228
  </form>
@@ -5,6 +5,7 @@ import { computed, ref } from 'vue'
5
5
  // (`/reset-password?token=…`). It is a public route (see AuthGate), so a recipient who
6
6
  // is signed out can still set a new password. On success we send them to the login.
7
7
  const auth = useAuthStore()
8
+ const { t } = useI18n()
8
9
 
9
10
  const token = computed(() => {
10
11
  if (typeof window === 'undefined') return ''
@@ -20,11 +21,11 @@ const done = ref(false)
20
21
  async function submit() {
21
22
  error.value = null
22
23
  if (password.value.length < 8) {
23
- error.value = 'Password must be at least 8 characters.'
24
+ error.value = t('auth.resetPassword.errorTooShort')
24
25
  return
25
26
  }
26
27
  if (password.value !== confirm.value) {
27
- error.value = 'Passwords do not match.'
28
+ error.value = t('auth.resetPassword.errorMismatch')
28
29
  return
29
30
  }
30
31
  busy.value = true
@@ -34,7 +35,7 @@ async function submit() {
34
35
  } catch (e) {
35
36
  error.value =
36
37
  (e as { data?: { error?: { message?: string } } })?.data?.error?.message ??
37
- 'This password reset link is invalid or has expired.'
38
+ t('auth.resetPassword.errorInvalidLink')
38
39
  } finally {
39
40
  busy.value = false
40
41
  }
@@ -52,23 +53,25 @@ function goToLogin() {
52
53
  >
53
54
  <div class="mb-6 text-center">
54
55
  <UIcon name="i-lucide-key-round" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
55
- <h1 class="mb-1 text-lg font-semibold text-white">Reset password</h1>
56
- <p class="text-sm text-slate-400">Choose a new password for your account.</p>
56
+ <h1 class="mb-1 text-lg font-semibold text-white">{{ t('auth.resetPassword.title') }}</h1>
57
+ <p class="text-sm text-slate-400">{{ t('auth.resetPassword.subtitle') }}</p>
57
58
  </div>
58
59
 
59
60
  <template v-if="done">
60
61
  <p class="mb-4 text-sm text-slate-300">
61
- Your password has been reset. You can now sign in with your new password.
62
+ {{ t('auth.resetPassword.doneBody') }}
62
63
  </p>
63
- <UButton block size="lg" color="primary" @click="goToLogin">Go to sign in</UButton>
64
+ <UButton block size="lg" color="primary" @click="goToLogin">{{
65
+ t('auth.resetPassword.goToSignIn')
66
+ }}</UButton>
64
67
  </template>
65
68
 
66
69
  <template v-else-if="!token">
67
70
  <p class="mb-4 text-sm text-rose-400">
68
- This reset link is missing its token. Request a new link from the sign-in screen.
71
+ {{ t('auth.resetPassword.missingToken') }}
69
72
  </p>
70
73
  <UButton block size="lg" color="neutral" variant="subtle" @click="goToLogin">
71
- Back to sign in
74
+ {{ t('auth.resetPassword.backToSignIn') }}
72
75
  </UButton>
73
76
  </template>
74
77
 
@@ -77,7 +80,7 @@ function goToLogin() {
77
80
  v-model="password"
78
81
  type="password"
79
82
  required
80
- placeholder="New password"
83
+ :placeholder="t('auth.resetPassword.newPasswordPlaceholder')"
81
84
  icon="i-lucide-lock"
82
85
  size="lg"
83
86
  class="w-full"
@@ -86,18 +89,18 @@ function goToLogin() {
86
89
  v-model="confirm"
87
90
  type="password"
88
91
  required
89
- placeholder="Confirm new password"
92
+ :placeholder="t('auth.resetPassword.confirmPasswordPlaceholder')"
90
93
  icon="i-lucide-lock"
91
94
  size="lg"
92
95
  class="w-full"
93
96
  />
94
97
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
95
98
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
96
- Reset password
99
+ {{ t('auth.resetPassword.submit') }}
97
100
  </UButton>
98
101
  <p class="text-center text-xs text-slate-400">
99
102
  <button type="button" class="text-indigo-400 hover:underline" @click="goToLogin">
100
- Back to sign in
103
+ {{ t('auth.resetPassword.backToSignIn') }}
101
104
  </button>
102
105
  </p>
103
106
  </form>
@@ -4,20 +4,21 @@ import type { DropdownMenuItem } from '@nuxt/ui'
4
4
  // Signed-in identity + per-user actions, shown in the sidebar when auth is enabled.
5
5
  const auth = useAuthStore()
6
6
  const ui = useUiStore()
7
+ const { t } = useI18n()
7
8
 
8
9
  const items = computed<DropdownMenuItem[][]>(() => [
9
10
  [
10
11
  {
11
12
  // The user-scoped "My setup" hub (personal GitHub token, local runners, personal
12
13
  // subscriptions) — kept out of the workspace Integrations hub, reachable here.
13
- label: 'My setup',
14
+ label: t('auth.userMenu.mySetup'),
14
15
  icon: 'i-lucide-user-cog',
15
16
  onSelect: () => ui.openPersonalSetup(),
16
17
  },
17
18
  ],
18
19
  [
19
20
  {
20
- label: 'Sign out',
21
+ label: t('auth.userMenu.signOut'),
21
22
  icon: 'i-lucide-log-out',
22
23
  onSelect: () => auth.logout(),
23
24
  },
@@ -11,6 +11,7 @@ const props = defineProps<{ accountId: string }>()
11
11
 
12
12
  const store = useAccountSettingsStore()
13
13
  const toast = useToast()
14
+ const { t } = useI18n()
14
15
 
15
16
  const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
16
17
  const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
@@ -24,7 +25,7 @@ onMounted(async () => {
24
25
  await store.load(props.accountId)
25
26
  } catch (e) {
26
27
  toast.add({
27
- title: 'Could not load deployment settings',
28
+ title: t('layout.accountDeployment.loadFailed'),
28
29
  description: e instanceof Error ? e.message : String(e),
29
30
  icon: 'i-lucide-triangle-alert',
30
31
  color: 'error',
@@ -34,7 +35,7 @@ onMounted(async () => {
34
35
 
35
36
  async function saveSlack() {
36
37
  if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
37
- toast.add({ title: 'Enter the client id, secret and redirect URL', color: 'error' })
38
+ toast.add({ title: t('layout.accountDeployment.slack.validation'), color: 'error' })
38
39
  return
39
40
  }
40
41
  savingSlack.value = true
@@ -51,10 +52,14 @@ async function saveSlack() {
51
52
  slack.clientId = ''
52
53
  slack.clientSecret = ''
53
54
  slack.redirectUrl = ''
54
- toast.add({ title: 'Slack OAuth saved', icon: 'i-lucide-check', color: 'success' })
55
+ toast.add({
56
+ title: t('layout.accountDeployment.slack.saved'),
57
+ icon: 'i-lucide-check',
58
+ color: 'success',
59
+ })
55
60
  } catch (e) {
56
61
  toast.add({
57
- title: 'Could not save Slack OAuth',
62
+ title: t('layout.accountDeployment.slack.saveFailed'),
58
63
  description: e instanceof Error ? e.message : String(e),
59
64
  color: 'error',
60
65
  })
@@ -67,10 +72,14 @@ async function clearSlack() {
67
72
  savingSlack.value = true
68
73
  try {
69
74
  await store.save(props.accountId, { secrets: { slackOAuth: null } })
70
- toast.add({ title: 'Slack OAuth cleared', icon: 'i-lucide-check', color: 'success' })
75
+ toast.add({
76
+ title: t('layout.accountDeployment.slack.cleared'),
77
+ icon: 'i-lucide-check',
78
+ color: 'success',
79
+ })
71
80
  } catch (e) {
72
81
  toast.add({
73
- title: 'Could not clear Slack OAuth',
82
+ title: t('layout.accountDeployment.slack.clearFailed'),
74
83
  description: e instanceof Error ? e.message : String(e),
75
84
  color: 'error',
76
85
  })
@@ -83,7 +92,7 @@ async function saveWeb() {
83
92
  const brave = web.braveApiKey.trim()
84
93
  const searxng = web.searxngUrl.trim()
85
94
  if (!brave && !searxng) {
86
- toast.add({ title: 'Enter a Brave key or a SearXNG URL', color: 'error' })
95
+ toast.add({ title: t('layout.accountDeployment.web.validation'), color: 'error' })
87
96
  return
88
97
  }
89
98
  savingWeb.value = true
@@ -100,10 +109,14 @@ async function saveWeb() {
100
109
  web.braveApiKey = ''
101
110
  web.searxngUrl = ''
102
111
  web.searxngApiKey = ''
103
- toast.add({ title: 'Web search keys saved', icon: 'i-lucide-check', color: 'success' })
112
+ toast.add({
113
+ title: t('layout.accountDeployment.web.saved'),
114
+ icon: 'i-lucide-check',
115
+ color: 'success',
116
+ })
104
117
  } catch (e) {
105
118
  toast.add({
106
- title: 'Could not save web search keys',
119
+ title: t('layout.accountDeployment.web.saveFailed'),
107
120
  description: e instanceof Error ? e.message : String(e),
108
121
  color: 'error',
109
122
  })
@@ -116,10 +129,14 @@ async function clearWeb() {
116
129
  savingWeb.value = true
117
130
  try {
118
131
  await store.save(props.accountId, { secrets: { webSearch: null } })
119
- toast.add({ title: 'Web search keys cleared', icon: 'i-lucide-check', color: 'success' })
132
+ toast.add({
133
+ title: t('layout.accountDeployment.web.cleared'),
134
+ icon: 'i-lucide-check',
135
+ color: 'success',
136
+ })
120
137
  } catch (e) {
121
138
  toast.add({
122
- title: 'Could not clear web search keys',
139
+ title: t('layout.accountDeployment.web.clearFailed'),
123
140
  description: e instanceof Error ? e.message : String(e),
124
141
  color: 'error',
125
142
  })
@@ -132,38 +149,50 @@ async function clearWeb() {
132
149
  <template>
133
150
  <div v-if="store.available !== false" class="space-y-6">
134
151
  <div>
135
- <h3 class="mb-1 font-semibold text-white">Deployment integrations</h3>
152
+ <h3 class="mb-1 font-semibold text-white">{{ t('layout.accountDeployment.title') }}</h3>
136
153
  <p class="text-[11px] text-slate-400">
137
- Credentials shared by every workspace in this account, sealed at rest. Values are never
138
- shown after saving; leave a field blank to keep the stored secret.
154
+ {{ t('layout.accountDeployment.intro') }}
139
155
  </p>
140
156
  </div>
141
157
 
142
158
  <!-- Slack app OAuth -->
143
159
  <section class="space-y-2">
144
160
  <div class="flex items-center gap-2">
145
- <h4 class="text-sm font-semibold text-slate-200">Slack app (OAuth)</h4>
161
+ <h4 class="text-sm font-semibold text-slate-200">
162
+ {{ t('layout.accountDeployment.slack.title') }}
163
+ </h4>
146
164
  <UBadge
147
165
  :color="summary?.slackOAuthConfigured ? 'success' : 'neutral'"
148
166
  variant="subtle"
149
167
  size="xs"
150
168
  >
151
- {{ summary?.slackOAuthConfigured ? 'Configured' : 'Not set' }}
169
+ {{
170
+ summary?.slackOAuthConfigured
171
+ ? t('layout.accountDeployment.configured')
172
+ : t('layout.accountDeployment.notSet')
173
+ }}
152
174
  </UBadge>
153
175
  </div>
154
176
  <p class="text-[11px] text-slate-400">
155
- Enables the "Add to Slack" OAuth flow. Without it, workspaces can still connect Slack by
156
- pasting a bot token.
177
+ {{ t('layout.accountDeployment.slack.description') }}
157
178
  </p>
158
179
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
159
- <UInput v-model="slack.clientId" placeholder="Client ID" size="sm" />
180
+ <UInput
181
+ v-model="slack.clientId"
182
+ :placeholder="t('layout.accountDeployment.slack.clientId')"
183
+ size="sm"
184
+ />
160
185
  <UInput
161
186
  v-model="slack.clientSecret"
162
187
  type="password"
163
- placeholder="Client secret"
188
+ :placeholder="t('layout.accountDeployment.slack.clientSecret')"
189
+ size="sm"
190
+ />
191
+ <UInput
192
+ v-model="slack.redirectUrl"
193
+ :placeholder="t('layout.accountDeployment.slack.redirectUrl')"
164
194
  size="sm"
165
195
  />
166
- <UInput v-model="slack.redirectUrl" placeholder="Redirect URL" size="sm" />
167
196
  </div>
168
197
  <div class="flex gap-2">
169
198
  <UButton
@@ -173,7 +202,7 @@ async function clearWeb() {
173
202
  :loading="savingSlack"
174
203
  @click="saveSlack"
175
204
  >
176
- Save
205
+ {{ t('common.save') }}
177
206
  </UButton>
178
207
  <UButton
179
208
  v-if="summary?.slackOAuthConfigured"
@@ -183,7 +212,7 @@ async function clearWeb() {
183
212
  :loading="savingSlack"
184
213
  @click="clearSlack"
185
214
  >
186
- Clear
215
+ {{ t('layout.accountDeployment.clear') }}
187
216
  </UButton>
188
217
  </div>
189
218
  </section>
@@ -191,22 +220,36 @@ async function clearWeb() {
191
220
  <!-- Web search keys -->
192
221
  <section class="space-y-2 border-t border-slate-800 pt-6">
193
222
  <div class="flex items-center gap-2">
194
- <h4 class="text-sm font-semibold text-slate-200">Container web search</h4>
223
+ <h4 class="text-sm font-semibold text-slate-200">
224
+ {{ t('layout.accountDeployment.web.title') }}
225
+ </h4>
195
226
  <UBadge :color="summary?.webSearch ? 'success' : 'neutral'" variant="subtle" size="xs">
196
- {{ summary?.webSearch ? `Configured (${summary.webSearch})` : 'Not set' }}
227
+ {{
228
+ summary?.webSearch
229
+ ? t('layout.accountDeployment.web.configured', { provider: summary.webSearch })
230
+ : t('layout.accountDeployment.notSet')
231
+ }}
197
232
  </UBadge>
198
233
  </div>
199
234
  <p class="text-[11px] text-slate-400">
200
- The search upstream container agents reach through the backend proxy. Set a Brave key
201
- (recommended), or a self-hosted SearXNG URL (with an optional bearer key).
235
+ {{ t('layout.accountDeployment.web.description') }}
202
236
  </p>
203
237
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
204
- <UInput v-model="web.braveApiKey" type="password" placeholder="Brave API key" size="sm" />
205
- <UInput v-model="web.searxngUrl" placeholder="SearXNG URL" size="sm" />
238
+ <UInput
239
+ v-model="web.braveApiKey"
240
+ type="password"
241
+ :placeholder="t('layout.accountDeployment.web.braveKey')"
242
+ size="sm"
243
+ />
244
+ <UInput
245
+ v-model="web.searxngUrl"
246
+ :placeholder="t('layout.accountDeployment.web.searxngUrl')"
247
+ size="sm"
248
+ />
206
249
  <UInput
207
250
  v-model="web.searxngApiKey"
208
251
  type="password"
209
- placeholder="SearXNG key (optional)"
252
+ :placeholder="t('layout.accountDeployment.web.searxngKey')"
210
253
  size="sm"
211
254
  />
212
255
  </div>
@@ -218,7 +261,7 @@ async function clearWeb() {
218
261
  :loading="savingWeb"
219
262
  @click="saveWeb"
220
263
  >
221
- Save
264
+ {{ t('common.save') }}
222
265
  </UButton>
223
266
  <UButton
224
267
  v-if="summary?.webSearch"
@@ -228,7 +271,7 @@ async function clearWeb() {
228
271
  :loading="savingWeb"
229
272
  @click="clearWeb"
230
273
  >
231
- Clear
274
+ {{ t('layout.accountDeployment.clear') }}
232
275
  </UButton>
233
276
  </div>
234
277
  </section>
@@ -8,6 +8,7 @@ import FragmentLibraryManager from '~/components/fragments/FragmentLibraryManage
8
8
 
9
9
  const props = defineProps<{ accountId: string }>()
10
10
  const workspace = useWorkspaceStore()
11
+ const { t } = useI18n()
11
12
 
12
13
  // Account-tier document fragments are fetched through a board's stored
13
14
  // document-source connection (credentials are per-workspace). Prefer the active
@@ -22,9 +23,7 @@ const viaWorkspaceId = computed(() => {
22
23
  <div class="space-y-6 text-sm">
23
24
  <section>
24
25
  <p class="mb-3 text-[11px] text-slate-400">
25
- Best-practice guidelines shared by every board in this account. Each board inherits these
26
- and can override or add its own. Code-aware agents (coder, reviewer, architect, fixers) fold
27
- the relevant ones into their prompt.
26
+ {{ t('layout.accountFragments.intro') }}
28
27
  </p>
29
28
  <FragmentLibraryManager
30
29
  kind="account"