@cat-factory/app 0.47.4 → 0.47.6

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.
@@ -21,6 +21,7 @@ const props = withDefaults(
21
21
  )
22
22
  const emit = defineEmits<{ 'update:modelValue': [string | undefined] }>()
23
23
 
24
+ const { t } = useI18n()
24
25
  const github = useGitHubStore()
25
26
  const toast = useToast()
26
27
 
@@ -51,7 +52,7 @@ async function browseTo(path: string) {
51
52
  } catch (e) {
52
53
  treeEntries.value = []
53
54
  toast.add({
54
- title: 'Could not list directory',
55
+ title: t('github.repoTree.errors.listDirectory'),
55
56
  description: e instanceof Error ? e.message : String(e),
56
57
  icon: 'i-lucide-triangle-alert',
57
58
  color: 'error',
@@ -85,7 +86,7 @@ watch(
85
86
  :disabled="loading"
86
87
  @click="browseTo('')"
87
88
  >
88
- root
89
+ {{ t('github.repoTree.root') }}
89
90
  </UButton>
90
91
  <template v-for="crumb in breadcrumbs" :key="crumb.path">
91
92
  <span class="text-slate-600">/</span>
@@ -103,9 +104,11 @@ watch(
103
104
 
104
105
  <!-- listing -->
105
106
  <div class="max-h-56 overflow-auto rounded border border-slate-800">
106
- <div v-if="loading" class="p-3 text-sm text-slate-400">Loading…</div>
107
+ <div v-if="loading" class="p-3 text-sm text-slate-400">
108
+ {{ t('github.repoTree.loading') }}
109
+ </div>
107
110
  <div v-else-if="isEmpty" class="p-3 text-sm text-slate-400">
108
- {{ mode === 'dir' ? 'No subdirectories here.' : 'Nothing here.' }}
111
+ {{ mode === 'dir' ? t('github.repoTree.noSubdirectories') : t('github.repoTree.empty') }}
109
112
  </div>
110
113
  <ul v-else class="divide-y divide-slate-800">
111
114
  <li
@@ -128,7 +131,11 @@ watch(
128
131
  :color="modelValue === entry.path ? 'primary' : 'neutral'"
129
132
  @click="pick(entry.path)"
130
133
  >
131
- {{ modelValue === entry.path ? 'Selected' : 'Select' }}
134
+ {{
135
+ modelValue === entry.path
136
+ ? t('github.repoTree.selected')
137
+ : t('github.repoTree.select')
138
+ }}
132
139
  </UButton>
133
140
  </li>
134
141
  <template v-if="mode === 'file'">
@@ -164,7 +171,7 @@ watch(
164
171
  :color="modelValue === currentPath ? 'primary' : 'neutral'"
165
172
  @click="pick(currentPath)"
166
173
  >
167
- Use this folder
174
+ {{ t('github.repoTree.useThisFolder') }}
168
175
  </UButton>
169
176
  </div>
170
177
  </div>
@@ -12,6 +12,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
12
12
  const ui = useUiStore()
13
13
  const slack = useSlackStore()
14
14
  const toast = useToast()
15
+ const { t } = useI18n()
15
16
 
16
17
  const open = computed({
17
18
  get: () => ui.slackOpen,
@@ -19,20 +20,23 @@ const open = computed({
19
20
  })
20
21
  const back = useIntegrationBack(open)
21
22
 
22
- const ROUTABLE: { type: NotificationType; label: string }[] = [
23
- { type: 'merge_review', label: 'Merge review' },
24
- { type: 'pipeline_complete', label: 'Pipeline complete' },
25
- { type: 'ci_failed', label: 'CI failed' },
26
- { type: 'test_failed', label: 'Tests failed' },
27
- { type: 'requirement_review', label: 'Requirement review' },
28
- { type: 'clarity_review', label: 'Clarity review' },
29
- { type: 'release_regression', label: 'Release regression' },
30
- { type: 'human_test_ready', label: 'Ready for human testing' },
31
- { type: 'visual_confirmation_ready', label: 'Ready for visual confirmation' },
32
- ]
23
+ const ROUTABLE = computed<{ type: NotificationType; label: string }[]>(() => [
24
+ { type: 'merge_review', label: t('slack.routable.merge_review') },
25
+ { type: 'pipeline_complete', label: t('slack.routable.pipeline_complete') },
26
+ { type: 'ci_failed', label: t('slack.routable.ci_failed') },
27
+ { type: 'test_failed', label: t('slack.routable.test_failed') },
28
+ { type: 'requirement_review', label: t('slack.routable.requirement_review') },
29
+ { type: 'clarity_review', label: t('slack.routable.clarity_review') },
30
+ { type: 'release_regression', label: t('slack.routable.release_regression') },
31
+ { type: 'human_test_ready', label: t('slack.routable.human_test_ready') },
32
+ { type: 'visual_confirmation_ready', label: t('slack.routable.visual_confirmation_ready') },
33
+ ])
33
34
 
34
35
  /** Notification-role options for a mapped member (drives who gets @-mentioned). */
35
- const ROLE_OPTIONS: SlackMemberRole[] = ['engineering', 'product']
36
+ const ROLE_OPTIONS = computed<{ label: string; value: SlackMemberRole }[]>(() => [
37
+ { label: t('slack.role.engineering'), value: 'engineering' },
38
+ { label: t('slack.role.product'), value: 'product' },
39
+ ])
36
40
 
37
41
  // Local editable copies, synced from the store on load.
38
42
  const routes = reactive<Record<NotificationType, SlackRoute>>({
@@ -71,13 +75,13 @@ watch(
71
75
  if (!isOpen || !slack.connected) return
72
76
  try {
73
77
  await Promise.all([slack.loadSettings(), slack.loadMemberMapping(), slack.loadChannels()])
74
- for (const { type } of ROUTABLE) {
78
+ for (const { type } of ROUTABLE.value) {
75
79
  routes[type] = slack.settings?.routes[type] ?? { enabled: false, channel: '' }
76
80
  }
77
81
  mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
78
82
  mapping.value = slack.memberMapping.map((e) => ({ role: 'engineering', ...e }))
79
83
  } catch (e) {
80
- notifyError('Could not load Slack settings', e)
84
+ notifyError(t('slack.error.loadSettings'), e)
81
85
  }
82
86
  },
83
87
  // Lazy v-if mount: the panel mounts with `open` already true, so load immediately.
@@ -88,7 +92,7 @@ async function connectViaOAuth() {
88
92
  try {
89
93
  window.location.href = await slack.installUrl()
90
94
  } catch (e) {
91
- notifyError('Could not start Slack OAuth', e)
95
+ notifyError(t('slack.error.startOAuth'), e)
92
96
  }
93
97
  }
94
98
 
@@ -97,9 +101,9 @@ async function connectWithToken() {
97
101
  try {
98
102
  await slack.connectWithToken(tokenInput.value.trim())
99
103
  tokenInput.value = ''
100
- toast.add({ title: 'Slack connected', icon: 'i-lucide-check', color: 'success' })
104
+ toast.add({ title: t('slack.toast.connected'), icon: 'i-lucide-check', color: 'success' })
101
105
  } catch (e) {
102
- notifyError('Could not connect Slack', e)
106
+ notifyError(t('slack.error.connect'), e)
103
107
  }
104
108
  }
105
109
 
@@ -107,7 +111,7 @@ async function disconnect() {
107
111
  try {
108
112
  await slack.disconnect()
109
113
  } catch (e) {
110
- notifyError('Could not disconnect Slack', e)
114
+ notifyError(t('slack.error.disconnect'), e)
111
115
  }
112
116
  }
113
117
 
@@ -118,9 +122,9 @@ async function saveRouting() {
118
122
  routes: { ...routes },
119
123
  mentionsEnabled: mentionsEnabled.value,
120
124
  })
121
- toast.add({ title: 'Routing saved', icon: 'i-lucide-check', color: 'success' })
125
+ toast.add({ title: t('slack.toast.routingSaved'), icon: 'i-lucide-check', color: 'success' })
122
126
  } catch (e) {
123
- notifyError('Could not save routing', e)
127
+ notifyError(t('slack.error.saveRouting'), e)
124
128
  } finally {
125
129
  busy.value = false
126
130
  }
@@ -138,9 +142,9 @@ async function saveMapping() {
138
142
  const entries = mapping.value.filter((e) => e.userId.trim() && e.slackUserId.trim())
139
143
  await slack.updateMemberMapping(entries)
140
144
  mapping.value = slack.memberMapping.map((e) => ({ ...e }))
141
- toast.add({ title: 'Member map saved', icon: 'i-lucide-check', color: 'success' })
145
+ toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
142
146
  } catch (e) {
143
- notifyError('Could not save member map', e)
147
+ notifyError(t('slack.error.saveMap'), e)
144
148
  } finally {
145
149
  busy.value = false
146
150
  }
@@ -148,15 +152,14 @@ async function saveMapping() {
148
152
  </script>
149
153
 
150
154
  <template>
151
- <UModal v-model:open="open" title="Slack notifications" :ui="{ content: 'max-w-2xl' }">
155
+ <UModal v-model:open="open" :title="t('slack.panel.title')" :ui="{ content: 'max-w-2xl' }">
152
156
  <template #title>
153
- <IntegrationBackTitle title="Slack notifications" @back="back" />
157
+ <IntegrationBackTitle :title="t('slack.panel.title')" @back="back" />
154
158
  </template>
155
159
  <template #body>
156
160
  <div class="space-y-5">
157
161
  <p class="text-xs text-slate-400">
158
- Post board notifications (merge reviews, pipeline completions, CI failures) to Slack. The
159
- connection is shared across the account; routing is per board.
162
+ {{ t('slack.panel.intro') }}
160
163
  </p>
161
164
 
162
165
  <!-- not connected: connect UI -->
@@ -167,11 +170,11 @@ async function saveMapping() {
167
170
  icon="i-lucide-slack"
168
171
  @click="connectViaOAuth"
169
172
  >
170
- Add to Slack
173
+ {{ t('slack.connect.addToSlack') }}
171
174
  </UButton>
172
175
  <div class="space-y-1">
173
176
  <span class="block text-[10px] uppercase tracking-wide text-slate-500">
174
- …or paste a bot token (xoxb-…)
177
+ {{ t('slack.connect.orPasteToken') }}
175
178
  </span>
176
179
  <div class="flex gap-2">
177
180
  <UInput
@@ -189,7 +192,7 @@ async function saveMapping() {
189
192
  :disabled="!tokenInput.trim()"
190
193
  @click="connectWithToken"
191
194
  >
192
- Connect
195
+ {{ t('slack.connect.connect') }}
193
196
  </UButton>
194
197
  </div>
195
198
  </div>
@@ -202,7 +205,11 @@ async function saveMapping() {
202
205
  >
203
206
  <UIcon name="i-lucide-slack" class="text-emerald-400" />
204
207
  <span class="flex-1 text-sm text-slate-200">
205
- Connected to <span class="font-semibold">{{ slack.connection?.teamName }}</span>
208
+ <i18n-t keypath="slack.connected.label" tag="span">
209
+ <template #team>
210
+ <span class="font-semibold">{{ slack.connection?.teamName }}</span>
211
+ </template>
212
+ </i18n-t>
206
213
  </span>
207
214
  <UButton
208
215
  color="error"
@@ -211,13 +218,15 @@ async function saveMapping() {
211
218
  icon="i-lucide-unplug"
212
219
  @click="disconnect"
213
220
  >
214
- Disconnect
221
+ {{ t('slack.connected.disconnect') }}
215
222
  </UButton>
216
223
  </div>
217
224
 
218
225
  <!-- routing -->
219
226
  <div class="space-y-3">
220
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">Routing</p>
227
+ <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
228
+ {{ t('slack.routing.heading') }}
229
+ </p>
221
230
  <div
222
231
  v-for="row in ROUTABLE"
223
232
  :key="row.type"
@@ -229,7 +238,7 @@ async function saveMapping() {
229
238
  v-model="routes[row.type]!.channel"
230
239
  size="sm"
231
240
  class="flex-1"
232
- placeholder="#channel or channel id"
241
+ :placeholder="t('slack.routing.channelPlaceholder')"
233
242
  :disabled="!routes[row.type]!.enabled"
234
243
  list="slack-channels"
235
244
  />
@@ -240,7 +249,7 @@ async function saveMapping() {
240
249
 
241
250
  <label class="flex items-center gap-2">
242
251
  <USwitch v-model="mentionsEnabled" size="sm" />
243
- <span class="text-sm text-slate-300">@-mention mapped account members</span>
252
+ <span class="text-sm text-slate-300">{{ t('slack.routing.mentionMembers') }}</span>
244
253
  </label>
245
254
 
246
255
  <div class="flex justify-end">
@@ -252,7 +261,7 @@ async function saveMapping() {
252
261
  :loading="busy"
253
262
  @click="saveRouting"
254
263
  >
255
- Save routing
264
+ {{ t('slack.routing.save') }}
256
265
  </UButton>
257
266
  </div>
258
267
  </div>
@@ -260,21 +269,37 @@ async function saveMapping() {
260
269
  <!-- member mapping -->
261
270
  <div v-if="mentionsEnabled" class="space-y-2">
262
271
  <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
263
- Member map (user id → Slack member id)
272
+ {{ t('slack.members.heading') }}
264
273
  </p>
265
274
  <p class="text-[11px] leading-snug text-slate-500">
266
- <span class="font-medium text-slate-400">Product</span> people are mentioned on
267
- requirement-review findings; everyone else only when they created the task.
275
+ <i18n-t keypath="slack.members.hint" tag="span">
276
+ <template #product>
277
+ <span class="font-medium text-slate-400">{{
278
+ t('slack.members.productLabel')
279
+ }}</span>
280
+ </template>
281
+ </i18n-t>
268
282
  </p>
269
283
  <div v-for="(entry, i) in mapping" :key="i" class="flex items-center gap-2">
270
- <UInput v-model="entry.userId" size="sm" class="w-40" placeholder="User id (usr_…)" />
284
+ <UInput
285
+ v-model="entry.userId"
286
+ size="sm"
287
+ class="w-40"
288
+ :placeholder="t('slack.members.userIdPlaceholder')"
289
+ />
271
290
  <UInput
272
291
  v-model="entry.slackUserId"
273
292
  size="sm"
274
293
  class="flex-1"
275
- placeholder="Slack member id (U…)"
294
+ :placeholder="t('slack.members.slackIdPlaceholder')"
295
+ />
296
+ <USelect
297
+ v-model="entry.role"
298
+ :items="ROLE_OPTIONS"
299
+ value-key="value"
300
+ size="sm"
301
+ class="w-32"
276
302
  />
277
- <USelect v-model="entry.role" :items="ROLE_OPTIONS" size="sm" class="w-32" />
278
303
  <UButton
279
304
  color="error"
280
305
  variant="ghost"
@@ -291,7 +316,7 @@ async function saveMapping() {
291
316
  icon="i-lucide-plus"
292
317
  @click="addMapping"
293
318
  >
294
- Add member
319
+ {{ t('slack.members.add') }}
295
320
  </UButton>
296
321
  <UButton
297
322
  color="primary"
@@ -301,7 +326,7 @@ async function saveMapping() {
301
326
  :loading="busy"
302
327
  @click="saveMapping"
303
328
  >
304
- Save map
329
+ {{ t('slack.members.save') }}
305
330
  </UButton>
306
331
  </div>
307
332
  </div>
@@ -20,6 +20,7 @@ const props = defineProps<{
20
20
  }>()
21
21
  const emit = defineEmits<{ pick: [item: PendingContext] }>()
22
22
 
23
+ const { t } = useI18n()
23
24
  const tasks = useTasksStore()
24
25
 
25
26
  const chosen = computed(() => new Set(props.chosenKeys ?? []))
@@ -186,28 +187,30 @@ onMounted(() => {
186
187
  class="w-full"
187
188
  :placeholder="
188
189
  searchable
189
- ? 'Search issues or paste a URL/key…'
190
- : (descriptor?.refPlaceholder ?? 'Paste an issue URL or key…')
190
+ ? t('tasks.picker.searchPlaceholder')
191
+ : (descriptor?.refPlaceholder ?? t('tasks.picker.refPlaceholder'))
191
192
  "
192
193
  @keydown.enter="refRow && pickRef(refRow)"
193
194
  />
194
195
 
195
196
  <p v-if="searchError" class="px-1 text-[11px] text-amber-400">
196
- Search failed: {{ searchError }}
197
+ {{ t('tasks.picker.searchFailed', { error: searchError }) }}
197
198
  </p>
198
199
 
199
200
  <div class="max-h-56 space-y-0.5 overflow-y-auto">
200
201
  <!-- Already-imported issues (linked directly, no re-fetch). -->
201
202
  <button
202
- v-for="t in importedRows"
203
- :key="`imp:${t.externalId}`"
203
+ v-for="row in importedRows"
204
+ :key="`imp:${row.externalId}`"
204
205
  type="button"
205
206
  class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
206
- @click="pickImported(t)"
207
+ @click="pickImported(row)"
207
208
  >
208
209
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
209
- <span class="truncate">{{ t.externalId }} · {{ t.title }}</span>
210
- <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">imported</UBadge>
210
+ <span class="truncate">{{ row.externalId }} · {{ row.title }}</span>
211
+ <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
212
+ t('tasks.picker.imported')
213
+ }}</UBadge>
211
214
  </button>
212
215
 
213
216
  <!-- Tracker search hits (imported on add). -->
@@ -233,18 +236,22 @@ onMounted(() => {
233
236
  @click="pickRef(refRow)"
234
237
  >
235
238
  <UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
236
- <span class="truncate"
237
- >Attach <span class="text-slate-200">{{ refRow }}</span> by reference</span
238
- >
239
+ <span class="truncate">
240
+ <i18n-t keypath="tasks.picker.attachByReference" tag="span" scope="global">
241
+ <template #ref>
242
+ <span class="text-slate-200">{{ refRow }}</span>
243
+ </template>
244
+ </i18n-t>
245
+ </span>
239
246
  </button>
240
247
 
241
248
  <p v-if="empty" class="px-2 py-1.5 text-[11px] text-slate-500">
242
249
  {{
243
250
  query.trim()
244
- ? 'No matching issues.'
251
+ ? t('tasks.picker.noMatches')
245
252
  : searchable
246
- ? 'Search by title, or pick an imported issue.'
247
- : 'Paste an issue URL or key to attach it.'
253
+ ? t('tasks.picker.emptySearchable')
254
+ : t('tasks.picker.emptyRefOnly')
248
255
  }}
249
256
  </p>
250
257
  </div>
@@ -9,6 +9,7 @@ import type { Block, TaskSourceKind } from '~/types/domain'
9
9
 
10
10
  const props = defineProps<{ block: Block }>()
11
11
 
12
+ const { t } = useI18n()
12
13
  const tasks = useTasksStore()
13
14
  const ui = useUiStore()
14
15
  const toast = useToast()
@@ -22,10 +23,10 @@ const linked = computed(() => tasks.tasksForBlock(props.block.id))
22
23
  async function attach(source: TaskSourceKind, externalId: string) {
23
24
  try {
24
25
  await tasks.linkToBlock(props.block.id, source, externalId)
25
- toast.add({ title: 'Issue attached', icon: 'i-lucide-link' })
26
+ toast.add({ title: t('tasks.contextIssues.attached'), icon: 'i-lucide-link' })
26
27
  } catch (e) {
27
28
  toast.add({
28
- title: 'Could not attach',
29
+ title: t('tasks.contextIssues.attachFailed'),
29
30
  description: e instanceof Error ? e.message : String(e),
30
31
  icon: 'i-lucide-triangle-alert',
31
32
  color: 'error',
@@ -43,7 +44,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
43
44
  onSelect: () => attach(t.source, t.externalId),
44
45
  }))
45
46
  items.push({
46
- label: 'Import an issue…',
47
+ label: t('tasks.contextIssues.importIssue'),
47
48
  icon: 'i-lucide-file-down',
48
49
  onSelect: () => ui.openTaskImport(),
49
50
  })
@@ -55,10 +56,12 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
55
56
  <div v-if="tasks.available" class="space-y-2">
56
57
  <div class="flex items-center justify-between">
57
58
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
58
- Context issues
59
+ {{ t('tasks.contextIssues.title') }}
59
60
  </span>
60
61
  <UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
61
- <UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">Attach</UButton>
62
+ <UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">{{
63
+ t('tasks.contextIssues.attach')
64
+ }}</UButton>
62
65
  </UDropdownMenu>
63
66
  </div>
64
67
 
@@ -82,7 +85,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
82
85
  </a>
83
86
  </div>
84
87
  <p v-else class="text-[11px] text-slate-500">
85
- Attach a Jira issue so agents see its description and comments while implementing this task.
88
+ {{ t('tasks.contextIssues.emptyHint') }}
86
89
  </p>
87
90
  </div>
88
91
  </template>