@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.
@@ -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()
@@ -60,9 +61,13 @@ const onBoardIds = computed(
60
61
  const repoItems = computed(() =>
61
62
  github.availableRepos.map((r) => {
62
63
  const onBoard = onBoardIds.value.has(r.githubId) && !r.isMonorepo
63
- 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('')
64
69
  return {
65
- label: `${r.owner}/${r.name}${r.private ? ' (private)' : ''}${mono}${onBoard ? ' · already on board' : ''}`,
70
+ label: `${r.owner}/${r.name}${suffix}`,
66
71
  // Searched on (lowercased once) — the owner/name, so the filter matches either.
67
72
  search: `${r.owner}/${r.name}`.toLowerCase(),
68
73
  value: r.githubId,
@@ -175,8 +180,8 @@ async function add() {
175
180
  configuredBlockId.value = block.id
176
181
  configuredDirectory.value = isMonorepo.value ? selectedDirectory.value : undefined
177
182
  toast.add({
178
- title: 'Service added',
179
- 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 }),
180
185
  icon: 'i-lucide-check',
181
186
  color: 'success',
182
187
  })
@@ -184,7 +189,7 @@ async function add() {
184
189
  selectedDirectory.value = undefined
185
190
  } catch (e) {
186
191
  toast.add({
187
- title: 'Could not add service',
192
+ title: t('github.addService.toast.addFailedTitle'),
188
193
  description: e instanceof Error ? e.message : String(e),
189
194
  icon: 'i-lucide-triangle-alert',
190
195
  color: 'error',
@@ -200,12 +205,11 @@ function done() {
200
205
  </script>
201
206
 
202
207
  <template>
203
- <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' }">
204
209
  <template #body>
205
210
  <div class="space-y-6">
206
211
  <p class="text-sm text-slate-400">
207
- Pick an existing GitHub repository to add as a board service. No bootstrapping — the repo
208
- is linked to a new service frame as-is, and tasks you run on it target that repo.
212
+ {{ t('github.addService.intro') }}
209
213
  </p>
210
214
 
211
215
  <!-- not connected: linking a repo needs the App bound to this workspace -->
@@ -216,8 +220,7 @@ function done() {
216
220
  <div class="flex items-start gap-2">
217
221
  <UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
218
222
  <p class="text-sm text-amber-200/90">
219
- Connect this workspace to GitHub first. Link an installation the App is already on, or
220
- install it.
223
+ {{ t('github.addService.connectFirst') }}
221
224
  </p>
222
225
  </div>
223
226
  <GitHubConnect />
@@ -225,18 +228,18 @@ function done() {
225
228
 
226
229
  <template v-else>
227
230
  <UFormField
228
- label="Repository"
229
- 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')"
230
233
  required
231
234
  >
232
235
  <div v-if="!hasRepos" class="text-sm text-slate-400">
233
- No repositories available yet — grant the App access to one below, then refresh.
236
+ {{ t('github.addService.noReposAvailable') }}
234
237
  </div>
235
238
  <div v-else class="space-y-1.5">
236
239
  <UInput
237
240
  v-model="repoSearch"
238
241
  icon="i-lucide-search"
239
- placeholder="Filter by owner/name…"
242
+ :placeholder="t('github.addService.filterPlaceholder')"
240
243
  class="w-full"
241
244
  :ui="{ trailing: 'pe-1' }"
242
245
  >
@@ -246,7 +249,7 @@ function done() {
246
249
  variant="link"
247
250
  size="sm"
248
251
  icon="i-lucide-x"
249
- aria-label="Clear filter"
252
+ :aria-label="t('github.addService.clearFilter')"
250
253
  @click="repoSearch = ''"
251
254
  />
252
255
  </template>
@@ -254,11 +257,16 @@ function done() {
254
257
  <USelect
255
258
  v-model="selectedRepoId"
256
259
  :items="filteredRepoItems"
257
- placeholder="Choose a repository"
260
+ :placeholder="t('github.addService.chooseRepository')"
258
261
  class="w-full"
259
262
  />
260
263
  <p class="text-xs text-slate-500">
261
- Showing {{ filteredRepoItems.length }} of {{ repoItems.length }} repositories.
264
+ {{
265
+ t('github.addService.showingCount', {
266
+ shown: filteredRepoItems.length,
267
+ total: repoItems.length,
268
+ })
269
+ }}
262
270
  </p>
263
271
  </div>
264
272
  </UFormField>
@@ -267,8 +275,8 @@ function done() {
267
275
  <div v-if="selectedRepoId !== undefined" class="space-y-3">
268
276
  <USwitch
269
277
  :model-value="isMonorepo"
270
- label="This is a monorepo (hosts more than one service)"
271
- description="Add several services from one repo, each pinned to a subdirectory."
278
+ :label="t('github.addService.monorepoLabel')"
279
+ :description="t('github.addService.monorepoDescription')"
272
280
  @update:model-value="toggleMonorepo"
273
281
  />
274
282
 
@@ -277,8 +285,7 @@ function done() {
277
285
  class="rounded-md border border-slate-700/60 bg-slate-900/40 p-3"
278
286
  >
279
287
  <p class="mb-2 text-xs text-slate-400">
280
- Browse the repository and pick the directory of the service you want to add. Agents
281
- working on this service will run within that subdirectory.
288
+ {{ t('github.addService.monorepoBrowseHint') }}
282
289
  </p>
283
290
  <RepoTreeBrowser
284
291
  v-model="selectedDirectory"
@@ -287,10 +294,10 @@ function done() {
287
294
  />
288
295
  <p class="mt-2 truncate text-xs text-slate-400">
289
296
  <template v-if="selectedDirectory">
290
- Service directory:
297
+ {{ t('github.addService.serviceDirectory') }}
291
298
  <code class="text-slate-200">{{ selectedDirectory }}</code>
292
299
  </template>
293
- <template v-else>No directory selected yet.</template>
300
+ <template v-else>{{ t('github.addService.noDirectorySelected') }}</template>
294
301
  </p>
295
302
  </div>
296
303
  </div>
@@ -304,7 +311,7 @@ function done() {
304
311
  class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
305
312
  >
306
313
  <UIcon name="i-lucide-check" class="h-3.5 w-3.5" />
307
- {{ configuredBlock.title }} added — configure it
314
+ {{ t('github.addService.addedConfigure', { title: configuredBlock.title }) }}
308
315
  </div>
309
316
  <ServiceTestConfig
310
317
  :block="configuredBlock"
@@ -321,10 +328,10 @@ function done() {
321
328
  size="sm"
322
329
  icon="i-lucide-shield-check"
323
330
  trailing-icon="i-lucide-external-link"
324
- title="Open the App's installation settings to grant it access to a repository"
331
+ :title="t('github.addService.grantAccessTitle')"
325
332
  @click="openManageInstall"
326
333
  >
327
- Grant the App access to a repo
334
+ {{ t('github.addService.grantAccess') }}
328
335
  </UButton>
329
336
  <UButton
330
337
  color="neutral"
@@ -334,13 +341,13 @@ function done() {
334
341
  :loading="github.loadingAvailable"
335
342
  @click="github.loadAvailableRepos()"
336
343
  >
337
- Refresh list
344
+ {{ t('github.addService.refreshList') }}
338
345
  </UButton>
339
346
  </div>
340
347
 
341
348
  <div class="flex justify-end gap-2">
342
349
  <UButton v-if="configuredBlock" color="neutral" variant="soft" size="sm" @click="done">
343
- Done
350
+ {{ t('github.addService.done') }}
344
351
  </UButton>
345
352
  <UButton
346
353
  v-if="!configuredBlock || isMonorepo"
@@ -350,7 +357,11 @@ function done() {
350
357
  :disabled="!canAdd"
351
358
  @click="add"
352
359
  >
353
- {{ configuredBlock && isMonorepo ? 'Add another service' : 'Add service' }}
360
+ {{
361
+ configuredBlock && isMonorepo
362
+ ? t('github.addService.addAnother')
363
+ : t('github.addService.add')
364
+ }}
354
365
  </UButton>
355
366
  </div>
356
367
  </template>