@cat-factory/app 0.47.3 → 0.47.5

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.
@@ -16,6 +16,7 @@ const props = defineProps<{
16
16
  }>()
17
17
  const emit = defineEmits<{ pick: [item: PendingContext] }>()
18
18
 
19
+ const { t } = useI18n()
19
20
  const documents = useDocumentsStore()
20
21
 
21
22
  const chosen = computed(() => new Set(props.chosenKeys ?? []))
@@ -182,14 +183,14 @@ onMounted(() => {
182
183
  class="w-full"
183
184
  :placeholder="
184
185
  searchable
185
- ? 'Search pages or paste a URL/ID…'
186
- : (descriptor?.refPlaceholder ?? 'Paste a page URL or ID…')
186
+ ? t('documents.picker.searchPlaceholder')
187
+ : (descriptor?.refPlaceholder ?? t('documents.picker.refPlaceholder'))
187
188
  "
188
189
  @keydown.enter="refRow && pickRef(refRow)"
189
190
  />
190
191
 
191
192
  <p v-if="searchError" class="px-1 text-[11px] text-amber-400">
192
- Search failed: {{ searchError }}
193
+ {{ t('documents.picker.searchFailed', { error: searchError }) }}
193
194
  </p>
194
195
 
195
196
  <div class="max-h-56 space-y-0.5 overflow-y-auto">
@@ -203,7 +204,9 @@ onMounted(() => {
203
204
  >
204
205
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
205
206
  <span class="truncate">{{ d.title }}</span>
206
- <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">imported</UBadge>
207
+ <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
208
+ t('documents.picker.importedBadge')
209
+ }}</UBadge>
207
210
  </button>
208
211
 
209
212
  <!-- Source search hits (imported on add). -->
@@ -226,18 +229,22 @@ onMounted(() => {
226
229
  @click="pickRef(refRow)"
227
230
  >
228
231
  <UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
229
- <span class="truncate"
230
- >Attach <span class="text-slate-200">{{ refRow }}</span> by reference</span
231
- >
232
+ <span class="truncate">
233
+ <i18n-t keypath="documents.picker.attachByReference" scope="global">
234
+ <template #ref>
235
+ <span class="text-slate-200">{{ refRow }}</span>
236
+ </template>
237
+ </i18n-t>
238
+ </span>
232
239
  </button>
233
240
 
234
241
  <p v-if="empty" class="px-2 py-1.5 text-[11px] text-slate-500">
235
242
  {{
236
243
  query.trim()
237
- ? 'No matching pages.'
244
+ ? t('documents.picker.noMatches')
238
245
  : searchable
239
- ? 'Search by title, or pick an imported document.'
240
- : 'Paste a page URL or ID to attach it.'
246
+ ? t('documents.picker.emptySearchable')
247
+ : t('documents.picker.emptyRefOnly')
241
248
  }}
242
249
  </p>
243
250
  </div>
@@ -6,6 +6,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
6
6
  // board structure. A source selector lets the user choose which connected source
7
7
  // to import from (Confluence, Notion, …). Carries an optional target frame from
8
8
  // the inspector so "Preview & spawn" lands the structure inside that frame.
9
+ const { t } = useI18n()
9
10
  const ui = useUiStore()
10
11
  const documents = useDocumentsStore()
11
12
  const board = useBoardStore()
@@ -60,10 +61,13 @@ async function doImport() {
60
61
  try {
61
62
  const doc = await documents.importDocument(source.value, value)
62
63
  ref_.value = ''
63
- toast.add({ title: `Imported "${doc.title}"`, icon: 'i-lucide-file-down' })
64
+ toast.add({
65
+ title: t('documents.import.imported', { title: doc.title }),
66
+ icon: 'i-lucide-file-down',
67
+ })
64
68
  } catch (e) {
65
69
  toast.add({
66
- title: 'Import failed',
70
+ title: t('documents.import.importFailed'),
67
71
  description: e instanceof Error ? e.message : String(e),
68
72
  icon: 'i-lucide-triangle-alert',
69
73
  color: 'error',
@@ -79,14 +83,14 @@ function preview(externalId: string) {
79
83
  </script>
80
84
 
81
85
  <template>
82
- <UModal v-model:open="open" title="Import from a document source">
86
+ <UModal v-model:open="open" :title="t('documents.import.title')">
83
87
  <template #title>
84
- <IntegrationBackTitle title="Import from a document source" @back="back" />
88
+ <IntegrationBackTitle :title="t('documents.import.title')" @back="back" />
85
89
  </template>
86
90
  <template #body>
87
91
  <div v-if="!documents.anyConnected" class="space-y-3 text-center">
88
92
  <UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
89
- <p class="text-sm text-slate-400">Connect a document source first.</p>
93
+ <p class="text-sm text-slate-400">{{ t('documents.import.connectFirst') }}</p>
90
94
  <div class="flex justify-center gap-2">
91
95
  <UButton
92
96
  v-for="s in documents.sources"
@@ -96,22 +100,29 @@ function preview(externalId: string) {
96
100
  :icon="s.icon"
97
101
  @click="ui.openDocumentConnect(s.source)"
98
102
  >
99
- Connect {{ s.label }}
103
+ {{ t('documents.import.connectSource', { source: s.label }) }}
100
104
  </UButton>
101
105
  </div>
102
106
  </div>
103
107
 
104
108
  <div v-else class="space-y-4">
105
109
  <p v-if="targetFrameTitle" class="text-xs text-slate-400">
106
- Spawning into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
110
+ <i18n-t keypath="documents.import.spawningInto" scope="global">
111
+ <template #frame>
112
+ <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
113
+ </template>
114
+ </i18n-t>
107
115
  </p>
108
116
 
109
- <UFormField v-if="sourceItems.length > 1" label="Source">
117
+ <UFormField v-if="sourceItems.length > 1" :label="t('documents.import.sourceLabel')">
110
118
  <USelect v-model="source" :items="sourceItems" class="w-full" />
111
119
  </UFormField>
112
120
 
113
121
  <div class="flex items-end gap-2">
114
- <UFormField :label="descriptor?.refLabel ?? 'Page URL or ID'" class="flex-1">
122
+ <UFormField
123
+ :label="descriptor?.refLabel ?? t('documents.import.refLabel')"
124
+ class="flex-1"
125
+ >
115
126
  <UInput
116
127
  v-model="ref_"
117
128
  :placeholder="descriptor?.refPlaceholder"
@@ -126,13 +137,13 @@ function preview(externalId: string) {
126
137
  :disabled="!ref_.trim()"
127
138
  @click="doImport"
128
139
  >
129
- Import
140
+ {{ t('documents.import.importButton') }}
130
141
  </UButton>
131
142
  </div>
132
143
 
133
144
  <div v-if="sourceDocs.length" class="space-y-2">
134
145
  <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
135
- Imported documents
146
+ {{ t('documents.import.importedHeading') }}
136
147
  </h3>
137
148
  <div
138
149
  v-for="doc in sourceDocs"
@@ -158,12 +169,14 @@ function preview(externalId: string) {
158
169
  icon="i-lucide-wand-sparkles"
159
170
  @click="preview(doc.externalId)"
160
171
  >
161
- Preview &amp; spawn
172
+ {{ t('documents.import.previewSpawn') }}
162
173
  </UButton>
163
174
  </div>
164
175
  </div>
165
176
  </div>
166
- <p v-else class="text-center text-xs text-slate-500">No documents imported yet.</p>
177
+ <p v-else class="text-center text-xs text-slate-500">
178
+ {{ t('documents.import.noneImported') }}
179
+ </p>
167
180
  </div>
168
181
  </template>
169
182
  </UModal>
@@ -6,6 +6,7 @@
6
6
  // "Connected" with empty fields.
7
7
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
8
8
 
9
+ const { t } = useI18n()
9
10
  const ui = useUiStore()
10
11
  const documents = useDocumentsStore()
11
12
  const toast = useToast()
@@ -54,14 +55,14 @@ async function submit() {
54
55
  try {
55
56
  await documents.connect(source.value, credentials)
56
57
  toast.add({
57
- title: `${descriptor.value!.label} connected`,
58
+ title: t('documents.connect.connected', { source: descriptor.value!.label }),
58
59
  icon: 'i-lucide-check',
59
60
  color: 'success',
60
61
  })
61
62
  ui.closeDocumentConnect()
62
63
  } catch (e) {
63
64
  toast.add({
64
- title: 'Could not connect',
65
+ title: t('documents.connect.connectFailed'),
65
66
  description: e instanceof Error ? e.message : String(e),
66
67
  icon: 'i-lucide-triangle-alert',
67
68
  color: 'error',
@@ -75,7 +76,9 @@ async function disconnect() {
75
76
  if (!source.value) return
76
77
  await documents.disconnect(source.value)
77
78
  toast.add({
78
- title: `${descriptor.value?.label ?? 'Source'} disconnected`,
79
+ title: t('documents.connect.disconnected', {
80
+ source: descriptor.value?.label ?? t('documents.connect.sourceFallback'),
81
+ }),
79
82
  icon: 'i-lucide-unplug',
80
83
  })
81
84
  ui.closeDocumentConnect()
@@ -83,15 +86,17 @@ async function disconnect() {
83
86
  </script>
84
87
 
85
88
  <template>
86
- <UModal v-model:open="open" :title="descriptor?.label ?? 'Connect source'">
89
+ <UModal v-model:open="open" :title="descriptor?.label ?? t('documents.connect.title')">
87
90
  <template #title>
88
- <IntegrationBackTitle :title="descriptor?.label ?? 'Connect source'" @back="back" />
91
+ <IntegrationBackTitle
92
+ :title="descriptor?.label ?? t('documents.connect.title')"
93
+ @back="back"
94
+ />
89
95
  </template>
90
96
  <template #body>
91
97
  <div v-if="descriptor" class="space-y-4">
92
98
  <p class="text-sm text-slate-400">
93
- Connect {{ descriptor.label }} to import requirements, RFCs and PRDs, then spawn board
94
- structure or attach them to tasks as agent context.
99
+ {{ t('documents.connect.intro', { source: descriptor.label }) }}
95
100
  </p>
96
101
 
97
102
  <p
@@ -101,8 +106,7 @@ async function disconnect() {
101
106
  >
102
107
  <UIcon name="i-lucide-user" class="mt-0.5 size-3.5 shrink-0" />
103
108
  <span>
104
- Personal connection — this credential authenticates as you and is stored only for your
105
- account, never shared with the rest of the workspace.
109
+ {{ t('documents.connect.personalNote') }}
106
110
  </span>
107
111
  </p>
108
112
 
@@ -130,7 +134,7 @@ async function disconnect() {
130
134
  icon="i-lucide-unplug"
131
135
  @click="disconnect"
132
136
  >
133
- Disconnect
137
+ {{ t('documents.connect.disconnect') }}
134
138
  </UButton>
135
139
  <div v-else />
136
140
  <UButton
@@ -140,7 +144,7 @@ async function disconnect() {
140
144
  :disabled="!canSubmit"
141
145
  @click="submit"
142
146
  >
143
- {{ connected ? 'Update connection' : 'Connect' }}
147
+ {{ connected ? t('documents.connect.update') : t('documents.connect.connect') }}
144
148
  </UButton>
145
149
  </div>
146
150
  </div>
@@ -4,6 +4,7 @@ import type { DocumentBoardPlan } from '~/types/domain'
4
4
  // Preview the structure an imported document expands into, then spawn it. The
5
5
  // plan is fetched fresh on open; a badge makes clear whether an LLM or the
6
6
  // deterministic heading parser produced it.
7
+ const { t } = useI18n()
7
8
  const ui = useUiStore()
8
9
  const documents = useDocumentsStore()
9
10
  const board = useBoardStore()
@@ -36,7 +37,7 @@ watch(
36
37
  plan.value = await documents.plan(preview.source, externalId)
37
38
  } catch (e) {
38
39
  toast.add({
39
- title: 'Could not build a plan',
40
+ title: t('documents.spawn.planFailed'),
40
41
  description: e instanceof Error ? e.message : String(e),
41
42
  icon: 'i-lucide-triangle-alert',
42
43
  color: 'error',
@@ -59,8 +60,12 @@ async function spawn() {
59
60
  targetFrameId.value ?? undefined,
60
61
  )
61
62
  toast.add({
62
- title: 'Structure spawned',
63
- description: `${result.frames} frames · ${result.modules} modules · ${result.tasks} tasks`,
63
+ title: t('documents.spawn.spawned'),
64
+ description: t('documents.spawn.summary', {
65
+ frames: t('documents.spawn.frameCount', { count: result.frames }, result.frames),
66
+ modules: t('documents.spawn.moduleCount', { count: result.modules }, result.modules),
67
+ tasks: t('documents.spawn.taskCount', { count: result.tasks }, result.tasks),
68
+ }),
64
69
  icon: 'i-lucide-check',
65
70
  color: 'success',
66
71
  })
@@ -68,7 +73,7 @@ async function spawn() {
68
73
  ui.closeDocumentImport()
69
74
  } catch (e) {
70
75
  toast.add({
71
- title: 'Spawn failed',
76
+ title: t('documents.spawn.spawnFailed'),
72
77
  description: e instanceof Error ? e.message : String(e),
73
78
  icon: 'i-lucide-triangle-alert',
74
79
  color: 'error',
@@ -80,7 +85,7 @@ async function spawn() {
80
85
  </script>
81
86
 
82
87
  <template>
83
- <UModal v-model:open="open" title="Preview structure">
88
+ <UModal v-model:open="open" :title="t('documents.spawn.title')">
84
89
  <template #body>
85
90
  <div class="space-y-4">
86
91
  <div v-if="plan" class="flex items-center justify-between gap-2">
@@ -89,16 +94,25 @@ async function spawn() {
89
94
  variant="subtle"
90
95
  size="sm"
91
96
  >
92
- {{ plan.planner === 'llm' ? 'AI-generated' : 'From headings' }}
97
+ {{
98
+ plan.planner === 'llm'
99
+ ? t('documents.spawn.plannerLlm')
100
+ : t('documents.spawn.plannerHeadings')
101
+ }}
93
102
  </UBadge>
94
103
  <span v-if="targetFrameTitle" class="text-xs text-slate-400">
95
- into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
104
+ <i18n-t keypath="documents.spawn.intoFrame" scope="global">
105
+ <template #frame>
106
+ <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
107
+ </template>
108
+ </i18n-t>
96
109
  </span>
97
- <span v-else class="text-xs text-slate-400">as new top-level frames</span>
110
+ <span v-else class="text-xs text-slate-400">{{ t('documents.spawn.asTopLevel') }}</span>
98
111
  </div>
99
112
 
100
113
  <div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-slate-400">
101
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Building plan…
114
+ <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
115
+ {{ t('documents.spawn.buildingPlan') }}
102
116
  </div>
103
117
 
104
118
  <div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pr-1">
@@ -144,7 +158,9 @@ async function spawn() {
144
158
  </div>
145
159
 
146
160
  <div class="flex justify-end gap-2 pt-1">
147
- <UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">Cancel</UButton>
161
+ <UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">{{
162
+ t('common.cancel')
163
+ }}</UButton>
148
164
  <UButton
149
165
  color="primary"
150
166
  icon="i-lucide-wand-sparkles"
@@ -152,7 +168,7 @@ async function spawn() {
152
168
  :disabled="!plan || loadingPlan"
153
169
  @click="spawn"
154
170
  >
155
- Spawn onto board
171
+ {{ t('documents.spawn.spawnOntoBoard') }}
156
172
  </UButton>
157
173
  </div>
158
174
  </div>
@@ -7,6 +7,7 @@ import type { Block, DocumentSourceKind } from '~/types/domain'
7
7
  // backend's userPromptFor). Rendered only when the integration is available.
8
8
  const props = defineProps<{ block: Block }>()
9
9
 
10
+ const { t } = useI18n()
10
11
  const documents = useDocumentsStore()
11
12
  const ui = useUiStore()
12
13
  const toast = useToast()
@@ -20,10 +21,10 @@ const linked = computed(() => documents.docsForBlock(props.block.id))
20
21
  async function attach(source: DocumentSourceKind, externalId: string) {
21
22
  try {
22
23
  await documents.linkToBlock(props.block.id, source, externalId)
23
- toast.add({ title: 'Document attached', icon: 'i-lucide-link' })
24
+ toast.add({ title: t('documents.taskDocs.attached'), icon: 'i-lucide-link' })
24
25
  } catch (e) {
25
26
  toast.add({
26
- title: 'Could not attach',
27
+ title: t('documents.taskDocs.attachFailed'),
27
28
  description: e instanceof Error ? e.message : String(e),
28
29
  icon: 'i-lucide-triangle-alert',
29
30
  color: 'error',
@@ -41,7 +42,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
41
42
  onSelect: () => attach(d.source, d.externalId),
42
43
  }))
43
44
  items.push({
44
- label: 'Import a page…',
45
+ label: t('documents.taskDocs.importPage'),
45
46
  icon: 'i-lucide-file-down',
46
47
  onSelect: () => ui.openDocumentImport(null),
47
48
  })
@@ -53,10 +54,12 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
53
54
  <div v-if="documents.available" class="space-y-2">
54
55
  <div class="flex items-center justify-between">
55
56
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
56
- Context documents
57
+ {{ t('documents.taskDocs.heading') }}
57
58
  </span>
58
59
  <UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
59
- <UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">Attach</UButton>
60
+ <UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">{{
61
+ t('documents.taskDocs.attach')
62
+ }}</UButton>
60
63
  </UDropdownMenu>
61
64
  </div>
62
65
 
@@ -77,7 +80,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
77
80
  </a>
78
81
  </div>
79
82
  <p v-else class="text-[11px] text-slate-500">
80
- Attach a requirement, RFC or PRD so agents see it while implementing this task.
83
+ {{ t('documents.taskDocs.empty') }}
81
84
  </p>
82
85
  </div>
83
86
  </template>
@@ -15,6 +15,7 @@ import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
15
15
  import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
16
16
  import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
17
17
 
18
+ const { t } = useI18n()
18
19
  const ui = useUiStore()
19
20
  const github = useGitHubStore()
20
21
  const board = useBoardStore()
@@ -39,18 +40,6 @@ async function loadRepos() {
39
40
  }
40
41
  }
41
42
 
42
- // On open: ensure we know the connection + which repos the App can access, and
43
- // the workspace's already-tracked repos (to flag ones already on the board).
44
- watch(
45
- open,
46
- (isOpen) => {
47
- if (!isOpen) return
48
- resetSelection()
49
- void loadRepos()
50
- },
51
- { immediate: true },
52
- )
53
-
54
43
  // If the user connects from inside the modal (the not-connected prompt), pull the
55
44
  // repo list as soon as the connection is bound.
56
45
  watch(
@@ -72,9 +61,13 @@ const onBoardIds = computed(
72
61
  const repoItems = computed(() =>
73
62
  github.availableRepos.map((r) => {
74
63
  const onBoard = onBoardIds.value.has(r.githubId) && !r.isMonorepo
75
- const mono = r.isMonorepo ? ' · monorepo' : ''
64
+ const suffix = [
65
+ r.private ? t('github.addService.repoLabel.private') : '',
66
+ r.isMonorepo ? t('github.addService.repoLabel.monorepo') : '',
67
+ onBoard ? t('github.addService.repoLabel.onBoard') : '',
68
+ ].join('')
76
69
  return {
77
- label: `${r.owner}/${r.name}${r.private ? ' (private)' : ''}${mono}${onBoard ? ' · already on board' : ''}`,
70
+ label: `${r.owner}/${r.name}${suffix}`,
78
71
  // Searched on (lowercased once) — the owner/name, so the filter matches either.
79
72
  search: `${r.owner}/${r.name}`.toLowerCase(),
80
73
  value: r.githubId,
@@ -152,6 +145,20 @@ const configuredBlock = computed(() =>
152
145
  configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
153
146
  )
154
147
 
148
+ // On open: ensure we know the connection + which repos the App can access, and
149
+ // the workspace's already-tracked repos (to flag ones already on the board).
150
+ // Declared after every ref resetSelection() touches so the `immediate` run
151
+ // doesn't access them inside their temporal dead zone.
152
+ watch(
153
+ open,
154
+ (isOpen) => {
155
+ if (!isOpen) return
156
+ resetSelection()
157
+ void loadRepos()
158
+ },
159
+ { immediate: true },
160
+ )
161
+
155
162
  // A monorepo service needs a chosen directory; a whole-repo service can be added once.
156
163
  const canAdd = computed(
157
164
  () =>
@@ -173,8 +180,8 @@ async function add() {
173
180
  configuredBlockId.value = block.id
174
181
  configuredDirectory.value = isMonorepo.value ? selectedDirectory.value : undefined
175
182
  toast.add({
176
- title: 'Service added',
177
- description: `${block.title} is on the board — configure it below.`,
183
+ title: t('github.addService.toast.addedTitle'),
184
+ description: t('github.addService.toast.addedDescription', { title: block.title }),
178
185
  icon: 'i-lucide-check',
179
186
  color: 'success',
180
187
  })
@@ -182,7 +189,7 @@ async function add() {
182
189
  selectedDirectory.value = undefined
183
190
  } catch (e) {
184
191
  toast.add({
185
- title: 'Could not add service',
192
+ title: t('github.addService.toast.addFailedTitle'),
186
193
  description: e instanceof Error ? e.message : String(e),
187
194
  icon: 'i-lucide-triangle-alert',
188
195
  color: 'error',
@@ -198,12 +205,11 @@ function done() {
198
205
  </script>
199
206
 
200
207
  <template>
201
- <UModal v-model:open="open" title="Add a service from a repository" :ui="{ content: 'max-w-xl' }">
208
+ <UModal v-model:open="open" :title="t('github.addService.title')" :ui="{ content: 'max-w-xl' }">
202
209
  <template #body>
203
210
  <div class="space-y-6">
204
211
  <p class="text-sm text-slate-400">
205
- Pick an existing GitHub repository to add as a board service. No bootstrapping — the repo
206
- is linked to a new service frame as-is, and tasks you run on it target that repo.
212
+ {{ t('github.addService.intro') }}
207
213
  </p>
208
214
 
209
215
  <!-- not connected: linking a repo needs the App bound to this workspace -->
@@ -214,8 +220,7 @@ function done() {
214
220
  <div class="flex items-start gap-2">
215
221
  <UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
216
222
  <p class="text-sm text-amber-200/90">
217
- Connect this workspace to GitHub first. Link an installation the App is already on, or
218
- install it.
223
+ {{ t('github.addService.connectFirst') }}
219
224
  </p>
220
225
  </div>
221
226
  <GitHubConnect />
@@ -223,18 +228,18 @@ function done() {
223
228
 
224
229
  <template v-else>
225
230
  <UFormField
226
- label="Repository"
227
- description="Repositories the GitHub App can access. Don't see yours? Grant the App access below, then refresh."
231
+ :label="t('github.addService.repository')"
232
+ :description="t('github.addService.repositoryHint')"
228
233
  required
229
234
  >
230
235
  <div v-if="!hasRepos" class="text-sm text-slate-400">
231
- No repositories available yet — grant the App access to one below, then refresh.
236
+ {{ t('github.addService.noReposAvailable') }}
232
237
  </div>
233
238
  <div v-else class="space-y-1.5">
234
239
  <UInput
235
240
  v-model="repoSearch"
236
241
  icon="i-lucide-search"
237
- placeholder="Filter by owner/name…"
242
+ :placeholder="t('github.addService.filterPlaceholder')"
238
243
  class="w-full"
239
244
  :ui="{ trailing: 'pe-1' }"
240
245
  >
@@ -244,7 +249,7 @@ function done() {
244
249
  variant="link"
245
250
  size="sm"
246
251
  icon="i-lucide-x"
247
- aria-label="Clear filter"
252
+ :aria-label="t('github.addService.clearFilter')"
248
253
  @click="repoSearch = ''"
249
254
  />
250
255
  </template>
@@ -252,11 +257,16 @@ function done() {
252
257
  <USelect
253
258
  v-model="selectedRepoId"
254
259
  :items="filteredRepoItems"
255
- placeholder="Choose a repository"
260
+ :placeholder="t('github.addService.chooseRepository')"
256
261
  class="w-full"
257
262
  />
258
263
  <p class="text-xs text-slate-500">
259
- Showing {{ filteredRepoItems.length }} of {{ repoItems.length }} repositories.
264
+ {{
265
+ t('github.addService.showingCount', {
266
+ shown: filteredRepoItems.length,
267
+ total: repoItems.length,
268
+ })
269
+ }}
260
270
  </p>
261
271
  </div>
262
272
  </UFormField>
@@ -265,8 +275,8 @@ function done() {
265
275
  <div v-if="selectedRepoId !== undefined" class="space-y-3">
266
276
  <USwitch
267
277
  :model-value="isMonorepo"
268
- label="This is a monorepo (hosts more than one service)"
269
- description="Add several services from one repo, each pinned to a subdirectory."
278
+ :label="t('github.addService.monorepoLabel')"
279
+ :description="t('github.addService.monorepoDescription')"
270
280
  @update:model-value="toggleMonorepo"
271
281
  />
272
282
 
@@ -275,8 +285,7 @@ function done() {
275
285
  class="rounded-md border border-slate-700/60 bg-slate-900/40 p-3"
276
286
  >
277
287
  <p class="mb-2 text-xs text-slate-400">
278
- Browse the repository and pick the directory of the service you want to add. Agents
279
- working on this service will run within that subdirectory.
288
+ {{ t('github.addService.monorepoBrowseHint') }}
280
289
  </p>
281
290
  <RepoTreeBrowser
282
291
  v-model="selectedDirectory"
@@ -285,10 +294,10 @@ function done() {
285
294
  />
286
295
  <p class="mt-2 truncate text-xs text-slate-400">
287
296
  <template v-if="selectedDirectory">
288
- Service directory:
297
+ {{ t('github.addService.serviceDirectory') }}
289
298
  <code class="text-slate-200">{{ selectedDirectory }}</code>
290
299
  </template>
291
- <template v-else>No directory selected yet.</template>
300
+ <template v-else>{{ t('github.addService.noDirectorySelected') }}</template>
292
301
  </p>
293
302
  </div>
294
303
  </div>
@@ -302,7 +311,7 @@ function done() {
302
311
  class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
303
312
  >
304
313
  <UIcon name="i-lucide-check" class="h-3.5 w-3.5" />
305
- {{ configuredBlock.title }} added — configure it
314
+ {{ t('github.addService.addedConfigure', { title: configuredBlock.title }) }}
306
315
  </div>
307
316
  <ServiceTestConfig
308
317
  :block="configuredBlock"
@@ -319,10 +328,10 @@ function done() {
319
328
  size="sm"
320
329
  icon="i-lucide-shield-check"
321
330
  trailing-icon="i-lucide-external-link"
322
- title="Open the App's installation settings to grant it access to a repository"
331
+ :title="t('github.addService.grantAccessTitle')"
323
332
  @click="openManageInstall"
324
333
  >
325
- Grant the App access to a repo
334
+ {{ t('github.addService.grantAccess') }}
326
335
  </UButton>
327
336
  <UButton
328
337
  color="neutral"
@@ -332,13 +341,13 @@ function done() {
332
341
  :loading="github.loadingAvailable"
333
342
  @click="github.loadAvailableRepos()"
334
343
  >
335
- Refresh list
344
+ {{ t('github.addService.refreshList') }}
336
345
  </UButton>
337
346
  </div>
338
347
 
339
348
  <div class="flex justify-end gap-2">
340
349
  <UButton v-if="configuredBlock" color="neutral" variant="soft" size="sm" @click="done">
341
- Done
350
+ {{ t('github.addService.done') }}
342
351
  </UButton>
343
352
  <UButton
344
353
  v-if="!configuredBlock || isMonorepo"
@@ -348,7 +357,11 @@ function done() {
348
357
  :disabled="!canAdd"
349
358
  @click="add"
350
359
  >
351
- {{ configuredBlock && isMonorepo ? 'Add another service' : 'Add service' }}
360
+ {{
361
+ configuredBlock && isMonorepo
362
+ ? t('github.addService.addAnother')
363
+ : t('github.addService.add')
364
+ }}
352
365
  </UButton>
353
366
  </div>
354
367
  </template>