@cat-factory/app 0.46.12 → 0.47.1

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 (31) 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/AiPresetMismatchDialog.vue +26 -13
  5. package/app/components/providers/AiProviderOnboardingModal.vue +18 -15
  6. package/app/components/providers/ApiKeysSection.vue +105 -63
  7. package/app/components/providers/PersonalCredentialModal.vue +35 -29
  8. package/app/components/providers/PersonalSubscriptionSection.vue +17 -2
  9. package/app/components/providers/SignInRequiredNotice.vue +19 -0
  10. package/app/components/providers/VendorCredentialsModal.vue +82 -39
  11. package/app/components/settings/AccountSettingsPanel.vue +9 -6
  12. package/app/components/settings/IssueTrackerPanel.vue +114 -53
  13. package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
  14. package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
  15. package/app/components/settings/MergeThresholdsPanel.vue +93 -45
  16. package/app/components/settings/ModelConfigurationPanel.vue +62 -36
  17. package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
  18. package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
  19. package/app/components/settings/ProviderConnectionPanel.vue +115 -61
  20. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
  21. package/app/components/settings/UserSecretsSection.vue +37 -18
  22. package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
  23. package/app/composables/api/auth.ts +7 -0
  24. package/app/stores/auth.ts +25 -1
  25. package/app/types/accountSettings.ts +4 -0
  26. package/i18n/locales/en.json +663 -0
  27. package/i18n/locales/es.json +651 -0
  28. package/i18n/locales/fr.json +651 -0
  29. package/i18n/locales/pl.json +651 -0
  30. package/i18n/locales/uk.json +651 -0
  31. package/package.json +2 -2
@@ -9,6 +9,7 @@ import { computed, ref, watch } from 'vue'
9
9
  import type { SubscriptionVendor } from '~/types/domain'
10
10
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
11
11
 
12
+ const { t, n } = useI18n()
12
13
  const ui = useUiStore()
13
14
  const workspace = useWorkspaceStore()
14
15
  const creds = useVendorCredentialsStore()
@@ -25,27 +26,51 @@ const back = useIntegrationBack(open)
25
26
  // Initialised from the ui store so a caller can deep-link to a tab — the user-scoped
26
27
  // "My subscriptions" entry opens straight onto the `personal` tab.
27
28
  const activeTab = ref(ui.vendorCredentialsTab)
28
- const tabs = [
29
- { value: 'pool', label: 'Workspace pool', icon: 'i-lucide-users', slot: 'pool' },
30
- { value: 'direct', label: 'Direct providers', icon: 'i-lucide-key-round', slot: 'direct' },
31
- { value: 'proxy', label: 'Proxies', icon: 'i-lucide-route', slot: 'proxy' },
29
+ const tabs = computed(() => [
30
+ {
31
+ value: 'pool',
32
+ label: t('providers.vendorCredentials.tabs.pool'),
33
+ icon: 'i-lucide-users',
34
+ slot: 'pool',
35
+ },
36
+ {
37
+ value: 'direct',
38
+ label: t('providers.vendorCredentials.tabs.direct'),
39
+ icon: 'i-lucide-key-round',
40
+ slot: 'direct',
41
+ },
42
+ {
43
+ value: 'proxy',
44
+ label: t('providers.vendorCredentials.tabs.proxy'),
45
+ icon: 'i-lucide-route',
46
+ slot: 'proxy',
47
+ },
32
48
  {
33
49
  value: 'personal',
34
- label: 'Personal subscriptions',
50
+ label: t('providers.vendorCredentials.tabs.personal'),
35
51
  icon: 'i-lucide-user',
36
52
  slot: 'personal',
37
53
  },
38
- ]
54
+ ])
39
55
 
40
56
  // Only commercial coding-plan vendors that permit team/organization use are poolable here.
41
57
  // Claude, GLM and ChatGPT/Codex are licensed for individual use only, so they are connected
42
58
  // per-user in the "Personal subscriptions" section below (PersonalSubscriptionSection).
43
- const VENDORS: { value: SubscriptionVendor; label: string; harness: string }[] = [
44
- { value: 'kimi', label: 'Kimi — Moonshot coding plan', harness: 'Claude Code' },
45
- { value: 'deepseek', label: 'DeepSeek — coding plan', harness: 'Claude Code' },
46
- ]
59
+ // Labels resolve through i18n (reactive to the locale) via literal keys.
60
+ const VENDORS = computed<{ value: SubscriptionVendor; label: string; harness: string }[]>(() => [
61
+ {
62
+ value: 'kimi',
63
+ label: t('providers.vendorCredentials.vendors.kimi.label'),
64
+ harness: 'Claude Code',
65
+ },
66
+ {
67
+ value: 'deepseek',
68
+ label: t('providers.vendorCredentials.vendors.deepseek.label'),
69
+ harness: 'Claude Code',
70
+ },
71
+ ])
47
72
 
48
- const visibleVendors = computed(() => VENDORS)
73
+ const visibleVendors = computed(() => VENDORS.value)
49
74
 
50
75
  const vendor = ref<SubscriptionVendor>('kimi')
51
76
  const label = ref('')
@@ -68,22 +93,22 @@ const steps = computed<string[]>(() => {
68
93
  switch (vendor.value) {
69
94
  case 'kimi':
70
95
  return [
71
- 'Open your Moonshot (Kimi) coding-plan console and create an API key for the Anthropic-compatible endpoint.',
72
- 'Copy the API key. Agent steps will run via Claude Code against Moonshot’s Anthropic endpoint with full context.',
73
- 'Paste the key below.',
96
+ t('providers.vendorCredentials.vendors.kimi.step1'),
97
+ t('providers.vendorCredentials.vendors.kimi.step2'),
98
+ t('providers.vendorCredentials.vendors.kimi.step3'),
74
99
  ]
75
100
  case 'deepseek':
76
101
  return [
77
- 'Open your DeepSeek coding-plan console and create an API key for the Anthropic-compatible endpoint.',
78
- 'Copy the API key. Agent steps will run via Claude Code against DeepSeek’s Anthropic endpoint with full context.',
79
- 'Paste the key below.',
102
+ t('providers.vendorCredentials.vendors.deepseek.step1'),
103
+ t('providers.vendorCredentials.vendors.deepseek.step2'),
104
+ t('providers.vendorCredentials.vendors.deepseek.step3'),
80
105
  ]
81
106
  default:
82
107
  return []
83
108
  }
84
109
  })
85
110
 
86
- const tokenPlaceholder = computed(() => 'your coding-plan API key')
111
+ const tokenPlaceholder = computed(() => t('providers.vendorCredentials.tokenPlaceholder'))
87
112
 
88
113
  async function add() {
89
114
  if (!token.value.trim()) return
@@ -91,15 +116,21 @@ async function add() {
91
116
  try {
92
117
  await creds.add({
93
118
  vendor: vendor.value,
94
- label: label.value.trim() || `${vendor.value} token`,
119
+ label:
120
+ label.value.trim() ||
121
+ t('providers.vendorCredentials.defaultLabel', { vendor: vendor.value }),
95
122
  token: token.value.trim(),
96
123
  })
97
124
  token.value = ''
98
125
  label.value = ''
99
- toast.add({ title: 'Token connected', icon: 'i-lucide-check', color: 'success' })
126
+ toast.add({
127
+ title: t('providers.vendorCredentials.toast.connected'),
128
+ icon: 'i-lucide-check',
129
+ color: 'success',
130
+ })
100
131
  } catch (e) {
101
132
  toast.add({
102
- title: 'Could not connect token',
133
+ title: t('providers.vendorCredentials.toast.connectFailed'),
103
134
  description: e instanceof Error ? e.message : String(e),
104
135
  color: 'error',
105
136
  })
@@ -113,7 +144,7 @@ async function remove(id: string) {
113
144
  await creds.remove(id)
114
145
  } catch (e) {
115
146
  toast.add({
116
- title: 'Could not remove token',
147
+ title: t('providers.vendorCredentials.toast.removeFailed'),
117
148
  description: e instanceof Error ? e.message : String(e),
118
149
  color: 'error',
119
150
  })
@@ -121,14 +152,18 @@ async function remove(id: string) {
121
152
  }
122
153
 
123
154
  function vendorLabel(v: SubscriptionVendor): string {
124
- return VENDORS.find((x) => x.value === v)?.label ?? v
155
+ return VENDORS.value.find((x) => x.value === v)?.label ?? v
125
156
  }
126
157
  </script>
127
158
 
128
159
  <template>
129
- <UModal v-model:open="open" title="LLM Vendors" :ui="{ content: 'max-w-2xl' }">
160
+ <UModal
161
+ v-model:open="open"
162
+ :title="t('providers.vendorCredentials.title')"
163
+ :ui="{ content: 'max-w-2xl' }"
164
+ >
130
165
  <template #title>
131
- <IntegrationBackTitle title="LLM Vendors" @back="back" />
166
+ <IntegrationBackTitle :title="t('providers.vendorCredentials.title')" @back="back" />
132
167
  </template>
133
168
  <template #body>
134
169
  <UTabs
@@ -141,17 +176,12 @@ function vendorLabel(v: SubscriptionVendor): string {
141
176
  <template #pool>
142
177
  <div class="space-y-5">
143
178
  <p class="text-sm text-slate-400">
144
- Connect a <strong>commercial</strong> coding-plan subscription (Kimi, DeepSeek) that
145
- permits team/organization use to run agent steps on the Claude Code harness instead of
146
- an API key. Tokens are stored encrypted, pooled, and rotated by usage. Subscription
147
- models are flat-rate quota — they don’t draw on your spend budget. Individual-use
148
- subscriptions (Claude, GLM, ChatGPT/Codex) are connected per-user in the Personal
149
- subscriptions tab.
179
+ {{ t('providers.vendorCredentials.poolIntro') }}
150
180
  </p>
151
181
 
152
182
  <!-- vendor picker -->
153
183
  <div class="flex flex-wrap items-end gap-3">
154
- <UFormField label="Vendor">
184
+ <UFormField :label="t('providers.vendorCredentials.vendorField')">
155
185
  <USelect
156
186
  v-model="vendor"
157
187
  :items="visibleVendors.map((v) => ({ label: v.label, value: v.value }))"
@@ -169,10 +199,13 @@ function vendorLabel(v: SubscriptionVendor): string {
169
199
 
170
200
  <!-- add form -->
171
201
  <div class="space-y-2">
172
- <UFormField label="Label (optional)">
173
- <UInput v-model="label" placeholder="e.g. work account" />
202
+ <UFormField :label="t('providers.vendorCredentials.labelField')">
203
+ <UInput
204
+ v-model="label"
205
+ :placeholder="t('providers.vendorCredentials.labelPlaceholder')"
206
+ />
174
207
  </UFormField>
175
- <UFormField label="Token">
208
+ <UFormField :label="t('providers.vendorCredentials.tokenField')">
176
209
  <UTextarea
177
210
  v-model="token"
178
211
  :rows="3"
@@ -187,7 +220,7 @@ function vendorLabel(v: SubscriptionVendor): string {
187
220
  icon="i-lucide-plus"
188
221
  @click="add()"
189
222
  >
190
- Connect
223
+ {{ t('providers.vendorCredentials.connect') }}
191
224
  </UButton>
192
225
  </div>
193
226
  </div>
@@ -195,7 +228,9 @@ function vendorLabel(v: SubscriptionVendor): string {
195
228
  <!-- connected pool -->
196
229
  <div v-if="creds.credentials.length" class="space-y-2">
197
230
  <h4 class="text-xs font-semibold uppercase tracking-wide text-slate-500">
198
- Connected ({{ creds.credentials.length }})
231
+ {{
232
+ t('providers.vendorCredentials.connected', { count: creds.credentials.length })
233
+ }}
199
234
  </h4>
200
235
  <div
201
236
  v-for="c in creds.credentials"
@@ -206,8 +241,16 @@ function vendorLabel(v: SubscriptionVendor): string {
206
241
  <span class="font-medium text-slate-200">{{ c.label }}</span>
207
242
  <span class="ml-2 text-xs text-slate-500">{{ vendorLabel(c.vendor) }}</span>
208
243
  <div class="text-[11px] tabular-nums text-slate-500">
209
- {{ (c.inputTokens + c.outputTokens).toLocaleString() }} tok this window ·
210
- {{ c.requestCount }} run{{ c.requestCount === 1 ? '' : 's' }}
244
+ {{
245
+ t(
246
+ 'providers.vendorCredentials.usage',
247
+ {
248
+ tokens: n(c.inputTokens + c.outputTokens, 'decimal'),
249
+ count: c.requestCount,
250
+ },
251
+ c.requestCount,
252
+ )
253
+ }}
211
254
  </div>
212
255
  </div>
213
256
  <UButton
@@ -8,6 +8,7 @@
8
8
  import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
9
9
  import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
10
10
 
11
+ const { t } = useI18n()
11
12
  const ui = useUiStore()
12
13
  const accounts = useAccountsStore()
13
14
 
@@ -23,21 +24,23 @@ const activeTab = computed({
23
24
  set: (v: string) => ui.setAccountSettingsTab(v),
24
25
  })
25
26
 
26
- const tabs = [
27
- { value: 'team', label: 'Team & access', icon: 'i-lucide-users', slot: 'team' },
27
+ const tabs = computed(() => [
28
+ { value: 'team', label: t('settings.account.tabs.team'), icon: 'i-lucide-users', slot: 'team' },
28
29
  {
29
30
  value: 'fragments',
30
- label: 'Context fragments',
31
+ label: t('settings.account.tabs.fragments'),
31
32
  icon: 'i-lucide-book-marked',
32
33
  slot: 'fragments',
33
34
  },
34
- ]
35
+ ])
35
36
  </script>
36
37
 
37
38
  <template>
38
- <UModal v-model:open="open" title="Account settings" :ui="{ content: 'max-w-3xl' }">
39
+ <UModal v-model:open="open" :title="t('settings.account.title')" :ui="{ content: 'max-w-3xl' }">
39
40
  <template #body>
40
- <p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">No account selected.</p>
41
+ <p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">
42
+ {{ t('settings.account.noAccount') }}
43
+ </p>
41
44
  <UTabs
42
45
  v-else
43
46
  v-model="activeTab"
@@ -17,6 +17,7 @@
17
17
  import { computed, onMounted, ref, watch } from 'vue'
18
18
  import type { TaskSourceDiagnosticStatus, TaskSourceKind, TrackerKind } from '~/types/domain'
19
19
 
20
+ const { t } = useI18n()
20
21
  const tracker = useTrackerStore()
21
22
  const tasks = useTasksStore()
22
23
  const ui = useUiStore()
@@ -78,10 +79,14 @@ async function save() {
78
79
  writebackCommentOnPrOpen: commentOnPrOpen.value,
79
80
  writebackResolveOnMerge: resolveOnMerge.value,
80
81
  })
81
- toast.add({ title: 'Issue tracker saved', icon: 'i-lucide-check', color: 'success' })
82
+ toast.add({
83
+ title: t('settings.issueTracker.toast.saved'),
84
+ icon: 'i-lucide-check',
85
+ color: 'success',
86
+ })
82
87
  } catch (e) {
83
88
  toast.add({
84
- title: 'Could not save settings',
89
+ title: t('settings.issueTracker.toast.saveFailed'),
85
90
  description: e instanceof Error ? e.message : String(e),
86
91
  icon: 'i-lucide-triangle-alert',
87
92
  color: 'error',
@@ -100,7 +105,7 @@ async function toggleSource(source: TaskSourceKind, enabled: boolean) {
100
105
  await tasks.setEnabled(source, enabled)
101
106
  } catch (e) {
102
107
  toast.add({
103
- title: 'Could not update',
108
+ title: t('settings.issueTracker.toast.updateFailed'),
104
109
  description: e instanceof Error ? e.message : String(e),
105
110
  icon: 'i-lucide-triangle-alert',
106
111
  color: 'error',
@@ -118,12 +123,20 @@ const probeFailureHint = computed(() => {
118
123
  const err = tasks.probeError
119
124
  if (tasks.available !== false || !err) return null
120
125
  if (err.status === 503) {
121
- return 'The task-source integration is turned off on this deployment (its encryption key is not configured). Set ENCRYPTION_KEY on the backend to enable issue tracking.'
126
+ return t('settings.issueTracker.probeFailure.disabled')
122
127
  }
123
128
  if (err.status && err.status >= 500) {
124
- return `The issue-tracker service returned an error (HTTP ${err.status}): ${err.message}. This usually means the backend isn't fully migrated/configured.`
129
+ return t('settings.issueTracker.probeFailure.serverError', {
130
+ status: err.status,
131
+ message: err.message,
132
+ })
125
133
  }
126
- return `Couldn't load issue-tracker settings${err.status ? ` (HTTP ${err.status})` : ''}: ${err.message}`
134
+ return err.status
135
+ ? t('settings.issueTracker.probeFailure.loadFailedStatus', {
136
+ status: err.status,
137
+ message: err.message,
138
+ })
139
+ : t('settings.issueTracker.probeFailure.loadFailed', { message: err.message })
127
140
  })
128
141
 
129
142
  async function checkSetup(source: TaskSourceKind) {
@@ -131,7 +144,7 @@ async function checkSetup(source: TaskSourceKind) {
131
144
  await tasks.checkSetup(source)
132
145
  } catch (e) {
133
146
  toast.add({
134
- title: 'Check failed',
147
+ title: t('settings.issueTracker.toast.checkFailed'),
135
148
  description: e instanceof Error ? e.message : String(e),
136
149
  icon: 'i-lucide-triangle-alert',
137
150
  color: 'error',
@@ -163,17 +176,22 @@ const STATUS_UI: Record<
163
176
  color="error"
164
177
  variant="subtle"
165
178
  icon="i-lucide-triangle-alert"
166
- title="Issue tracking isn't available"
179
+ :title="t('settings.issueTracker.probeFailure.title')"
167
180
  :description="probeFailureHint"
168
181
  />
169
182
 
170
183
  <!-- 1. Filing tracker ----------------------------------------------------->
171
184
  <section class="space-y-3">
172
185
  <div>
173
- <h3 class="text-sm font-semibold text-slate-200">Where tickets are filed</h3>
186
+ <h3 class="text-sm font-semibold text-slate-200">
187
+ {{ t('settings.issueTracker.filing.heading') }}
188
+ </h3>
174
189
  <p class="mt-1 text-[11px] text-slate-400">
175
- The tech-debt recurring pipeline raises an issue before implementation and files it in
176
- this tracker. Choose <span class="text-slate-300">None</span> to skip filing.
190
+ <i18n-t keypath="settings.issueTracker.filing.description" tag="span" scope="global">
191
+ <template #none>
192
+ <span class="text-slate-300">{{ t('settings.issueTracker.filing.none') }}</span>
193
+ </template>
194
+ </i18n-t>
177
195
  </p>
178
196
  </div>
179
197
 
@@ -185,7 +203,7 @@ const STATUS_UI: Record<
185
203
  :variant="trackerKind === null ? 'solid' : 'subtle'"
186
204
  @click="trackerKind = null"
187
205
  >
188
- None
206
+ {{ t('settings.issueTracker.filing.none') }}
189
207
  </UButton>
190
208
  <UButton
191
209
  icon="i-lucide-github"
@@ -194,7 +212,7 @@ const STATUS_UI: Record<
194
212
  :variant="trackerKind === 'github' ? 'solid' : 'subtle'"
195
213
  @click="trackerKind = 'github'"
196
214
  >
197
- GitHub Issues
215
+ {{ t('settings.issueTracker.vendor.github') }}
198
216
  </UButton>
199
217
  <UButton
200
218
  icon="i-lucide-trello"
@@ -218,36 +236,58 @@ const STATUS_UI: Record<
218
236
 
219
237
  <!-- Inline readiness hints for the picked tracker. -->
220
238
  <p v-if="trackerKind === 'github' && !githubAvailable" class="text-[11px] text-amber-400">
221
- GitHub Issues rides your installed GitHub App, which isn't connected yet. Install it under
222
- <button class="underline" @click="ui.openGitHub()">Integrations → GitHub</button> — filing
223
- stays off until then.
239
+ <i18n-t keypath="settings.issueTracker.filing.githubHint" tag="span" scope="global">
240
+ <template #link>
241
+ <button class="underline" @click="ui.openGitHub()">
242
+ {{ t('settings.issueTracker.filing.githubHintLink') }}
243
+ </button>
244
+ </template>
245
+ </i18n-t>
224
246
  </p>
225
247
  <p v-else-if="trackerKind === 'jira' && !jiraConnected" class="text-[11px] text-amber-400">
226
- Jira isn't connected yet.
227
- <button class="underline" @click="ui.openTaskConnect('jira')">Connect it</button> to file
228
- and link issues.
248
+ <i18n-t keypath="settings.issueTracker.filing.jiraHint" tag="span" scope="global">
249
+ <template #link>
250
+ <button class="underline" @click="ui.openTaskConnect('jira')">
251
+ {{ t('settings.issueTracker.filing.connectLink') }}
252
+ </button>
253
+ </template>
254
+ </i18n-t>
229
255
  </p>
230
256
  <p
231
257
  v-else-if="trackerKind === 'linear' && !linearConnected"
232
258
  class="text-[11px] text-amber-400"
233
259
  >
234
- Linear isn't connected yet.
235
- <button class="underline" @click="ui.openTaskConnect('linear')">Connect it</button> to file
236
- and link issues.
260
+ <i18n-t keypath="settings.issueTracker.filing.linearHint" tag="span" scope="global">
261
+ <template #link>
262
+ <button class="underline" @click="ui.openTaskConnect('linear')">
263
+ {{ t('settings.issueTracker.filing.connectLink') }}
264
+ </button>
265
+ </template>
266
+ </i18n-t>
237
267
  </p>
238
268
 
239
- <UFormField v-if="trackerKind === 'jira'" label="Jira project key" class="w-48">
269
+ <UFormField
270
+ v-if="trackerKind === 'jira'"
271
+ :label="t('settings.issueTracker.filing.jiraProjectKey')"
272
+ class="w-48"
273
+ >
240
274
  <UInput v-model="jiraProjectKey" placeholder="ENG" size="sm" class="w-full" />
241
275
  <template #help>
242
- <span class="text-[11px] text-slate-500">New tickets are filed under this project.</span>
276
+ <span class="text-[11px] text-slate-500">
277
+ {{ t('settings.issueTracker.filing.jiraProjectKeyHelp') }}
278
+ </span>
243
279
  </template>
244
280
  </UFormField>
245
281
 
246
- <UFormField v-if="trackerKind === 'linear'" label="Linear team id" class="w-64">
282
+ <UFormField
283
+ v-if="trackerKind === 'linear'"
284
+ :label="t('settings.issueTracker.filing.linearTeamId')"
285
+ class="w-64"
286
+ >
247
287
  <UInput v-model="linearTeamId" placeholder="team_…" size="sm" class="w-full" />
248
288
  <template #help>
249
289
  <span class="text-[11px] text-slate-500">
250
- New issues are created under this team (Linear requires a team to create an issue).
290
+ {{ t('settings.issueTracker.filing.linearTeamIdHelp') }}
251
291
  </span>
252
292
  </template>
253
293
  </UFormField>
@@ -256,11 +296,11 @@ const STATUS_UI: Record<
256
296
  <!-- 2. Linking sources ---------------------------------------------------->
257
297
  <section class="space-y-3">
258
298
  <div>
259
- <h3 class="text-sm font-semibold text-slate-200">Link issues as context</h3>
299
+ <h3 class="text-sm font-semibold text-slate-200">
300
+ {{ t('settings.issueTracker.linking.heading') }}
301
+ </h3>
260
302
  <p class="mt-1 text-[11px] text-slate-400">
261
- When a source is offered you can import its issues and attach them to a task, so agents
262
- see the issue description and comments while implementing. This is independent of the
263
- filing tracker above.
303
+ {{ t('settings.issueTracker.linking.description') }}
264
304
  </p>
265
305
  </div>
266
306
 
@@ -270,12 +310,14 @@ const STATUS_UI: Record<
270
310
  <div class="flex min-w-0 items-center gap-2.5">
271
311
  <UIcon name="i-lucide-github" class="h-5 w-5 shrink-0 text-slate-300" />
272
312
  <div class="min-w-0">
273
- <div class="text-sm font-medium text-slate-200">GitHub Issues</div>
313
+ <div class="text-sm font-medium text-slate-200">
314
+ {{ t('settings.issueTracker.vendor.github') }}
315
+ </div>
274
316
  <div class="text-[11px] text-slate-500">
275
317
  {{
276
318
  githubAvailable
277
- ? 'Rides your installed GitHub App — no credentials needed.'
278
- : 'Install the GitHub App (Integrations → GitHub) to offer this source.'
319
+ ? t('settings.issueTracker.linking.github.available')
320
+ : t('settings.issueTracker.linking.github.unavailable')
279
321
  }}
280
322
  </div>
281
323
  </div>
@@ -289,7 +331,7 @@ const STATUS_UI: Record<
289
331
  :loading="tasks.checking === 'github'"
290
332
  @click="checkSetup('github')"
291
333
  >
292
- Check setup
334
+ {{ t('settings.issueTracker.linking.checkSetup') }}
293
335
  </UButton>
294
336
  <USwitch
295
337
  v-if="githubAvailable"
@@ -305,7 +347,7 @@ const STATUS_UI: Record<
305
347
  icon="i-lucide-github"
306
348
  @click="ui.openGitHub()"
307
349
  >
308
- Install
350
+ {{ t('settings.issueTracker.linking.install') }}
309
351
  </UButton>
310
352
  </div>
311
353
  </div>
@@ -329,9 +371,15 @@ const STATUS_UI: Record<
329
371
  <div class="flex min-w-0 items-center gap-2.5">
330
372
  <UIcon name="i-lucide-trello" class="h-5 w-5 shrink-0 text-slate-300" />
331
373
  <div class="min-w-0">
332
- <div class="text-sm font-medium text-slate-200">Jira</div>
374
+ <div class="text-sm font-medium text-slate-200">
375
+ {{ t('settings.issueTracker.vendor.jira') }}
376
+ </div>
333
377
  <div class="text-[11px] text-slate-500">
334
- {{ jiraConnected ? 'Connected.' : 'Connect with a Jira account and API token.' }}
378
+ {{
379
+ jiraConnected
380
+ ? t('settings.issueTracker.linking.connected')
381
+ : t('settings.issueTracker.linking.jira.connectHint')
382
+ }}
335
383
  </div>
336
384
  </div>
337
385
  </div>
@@ -345,7 +393,7 @@ const STATUS_UI: Record<
345
393
  :loading="tasks.checking === 'jira'"
346
394
  @click="checkSetup('jira')"
347
395
  >
348
- Check setup
396
+ {{ t('settings.issueTracker.linking.checkSetup') }}
349
397
  </UButton>
350
398
  <USwitch
351
399
  v-if="jira?.available"
@@ -361,7 +409,7 @@ const STATUS_UI: Record<
361
409
  icon="i-lucide-plug"
362
410
  @click="ui.openTaskConnect('jira')"
363
411
  >
364
- Connect
412
+ {{ t('settings.issueTracker.linking.connect') }}
365
413
  </UButton>
366
414
  </div>
367
415
  </div>
@@ -385,9 +433,15 @@ const STATUS_UI: Record<
385
433
  <div class="flex min-w-0 items-center gap-2.5">
386
434
  <UIcon name="i-lucide-square-kanban" class="h-5 w-5 shrink-0 text-slate-300" />
387
435
  <div class="min-w-0">
388
- <div class="text-sm font-medium text-slate-200">Linear</div>
436
+ <div class="text-sm font-medium text-slate-200">
437
+ {{ t('settings.issueTracker.vendor.linear') }}
438
+ </div>
389
439
  <div class="text-[11px] text-slate-500">
390
- {{ linearConnected ? 'Connected.' : 'Connect with a Linear personal API key.' }}
440
+ {{
441
+ linearConnected
442
+ ? t('settings.issueTracker.linking.connected')
443
+ : t('settings.issueTracker.linking.linear.connectHint')
444
+ }}
391
445
  </div>
392
446
  </div>
393
447
  </div>
@@ -401,7 +455,7 @@ const STATUS_UI: Record<
401
455
  :loading="tasks.checking === 'linear'"
402
456
  @click="checkSetup('linear')"
403
457
  >
404
- Check setup
458
+ {{ t('settings.issueTracker.linking.checkSetup') }}
405
459
  </UButton>
406
460
  <USwitch
407
461
  v-if="linear?.available"
@@ -417,7 +471,7 @@ const STATUS_UI: Record<
417
471
  icon="i-lucide-plug"
418
472
  @click="ui.openTaskConnect('linear')"
419
473
  >
420
- Connect
474
+ {{ t('settings.issueTracker.linking.connect') }}
421
475
  </UButton>
422
476
  </div>
423
477
  </div>
@@ -439,21 +493,26 @@ const STATUS_UI: Record<
439
493
  <!-- 3. Writeback ---------------------------------------------------------->
440
494
  <section class="space-y-3">
441
495
  <div>
442
- <h3 class="text-sm font-semibold text-slate-200">Writeback</h3>
496
+ <h3 class="text-sm font-semibold text-slate-200">
497
+ {{ t('settings.issueTracker.writeback.heading') }}
498
+ </h3>
443
499
  <p class="mt-1 text-[11px] text-slate-400">
444
- Write back to a task's linked issue(s) as its pull request progresses. Each toggle is the
445
- workspace default and can be overridden per task in the inspector. GitHub issues close
446
- natively; Jira issues transition to the first status in their
447
- <span class="text-slate-300">Done</span> category.
500
+ <i18n-t keypath="settings.issueTracker.writeback.description" tag="span" scope="global">
501
+ <template #done>
502
+ <span class="text-slate-300">{{ t('settings.issueTracker.writeback.done') }}</span>
503
+ </template>
504
+ </i18n-t>
448
505
  </p>
449
506
  </div>
450
507
 
451
508
  <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
452
509
  <USwitch v-model="commentOnPrOpen" />
453
510
  <span class="text-sm">
454
- <span class="block text-slate-200">Comment when a PR opens</span>
511
+ <span class="block text-slate-200">
512
+ {{ t('settings.issueTracker.writeback.commentOnOpen.label') }}
513
+ </span>
455
514
  <span class="block text-xs text-slate-500">
456
- Post a comment on the linked issue with the new pull request's link.
515
+ {{ t('settings.issueTracker.writeback.commentOnOpen.help') }}
457
516
  </span>
458
517
  </span>
459
518
  </label>
@@ -461,9 +520,11 @@ const STATUS_UI: Record<
461
520
  <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
462
521
  <USwitch v-model="resolveOnMerge" />
463
522
  <span class="text-sm">
464
- <span class="block text-slate-200">Close as resolved when a PR merges</span>
523
+ <span class="block text-slate-200">
524
+ {{ t('settings.issueTracker.writeback.resolveOnMerge.label') }}
525
+ </span>
465
526
  <span class="block text-xs text-slate-500">
466
- Comment that the PR merged, then close / resolve the linked issue.
527
+ {{ t('settings.issueTracker.writeback.resolveOnMerge.help') }}
467
528
  </span>
468
529
  </span>
469
530
  </label>
@@ -478,7 +539,7 @@ const STATUS_UI: Record<
478
539
  :disabled="!canSave"
479
540
  @click="save"
480
541
  >
481
- Save
542
+ {{ t('common.save') }}
482
543
  </UButton>
483
544
  </div>
484
545
  </div>