i18n-dashboard 0.1.0

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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +715 -0
  3. package/app.vue +8 -0
  4. package/assets/css/main.css +21 -0
  5. package/assets/locales/en.json +380 -0
  6. package/bin/cli.mjs +279 -0
  7. package/components/LinkedKeyPicker.vue +135 -0
  8. package/components/PathPicker.vue +153 -0
  9. package/components/PluralEditor.vue +295 -0
  10. package/components/ScanModal.vue +153 -0
  11. package/components/TranslationHistoryModal.vue +66 -0
  12. package/components/TranslationRow.vue +541 -0
  13. package/components/dashboard/WidgetConfigModal.vue +121 -0
  14. package/components/dashboard/WidgetGrid.vue +190 -0
  15. package/components/dashboard/WidgetPicker.vue +75 -0
  16. package/components/dashboard/widgets/ActivityWidget.vue +109 -0
  17. package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
  18. package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
  19. package/components/dashboard/widgets/ReviewWidget.vue +150 -0
  20. package/components/dashboard/widgets/StatWidget.vue +133 -0
  21. package/composables/useAuth.ts +72 -0
  22. package/composables/useConfig.ts +14 -0
  23. package/composables/useDashboard.ts +89 -0
  24. package/composables/useFormats.ts +100 -0
  25. package/composables/useKeys.ts +231 -0
  26. package/composables/useLanguages.ts +221 -0
  27. package/composables/useProfile.ts +76 -0
  28. package/composables/useProject.ts +180 -0
  29. package/composables/useReview.ts +94 -0
  30. package/composables/useSettings.ts +30 -0
  31. package/composables/useStats.ts +16 -0
  32. package/composables/useT.ts +38 -0
  33. package/composables/useUsers.ts +101 -0
  34. package/composables/useWidgetData.ts +50 -0
  35. package/consts/commons.const.ts +6 -0
  36. package/consts/dashboard.const.ts +94 -0
  37. package/consts/languages.const.ts +223 -0
  38. package/enums/commons.enum.ts +7 -0
  39. package/i18n-dashboard.config.example.js +40 -0
  40. package/interfaces/commons.interface.ts +23 -0
  41. package/interfaces/job.interface.ts +10 -0
  42. package/interfaces/key.interface.ts +39 -0
  43. package/interfaces/languages.interface.ts +23 -0
  44. package/interfaces/project.interface.ts +9 -0
  45. package/interfaces/scan.interface.ts +12 -0
  46. package/interfaces/settings.interface.ts +4 -0
  47. package/interfaces/stat.interface.ts +30 -0
  48. package/interfaces/translation.interface.ts +11 -0
  49. package/interfaces/user.interface.ts +24 -0
  50. package/layouts/auth.vue +5 -0
  51. package/layouts/default.vue +327 -0
  52. package/middleware/auth.global.ts +26 -0
  53. package/nuxt.config.ts +66 -0
  54. package/package.json +89 -0
  55. package/pages/index.vue +5 -0
  56. package/pages/login.vue +74 -0
  57. package/pages/onboarding.vue +563 -0
  58. package/pages/projects/[id]/formats/datetime.vue +240 -0
  59. package/pages/projects/[id]/formats/modifiers.vue +194 -0
  60. package/pages/projects/[id]/formats/number.vue +250 -0
  61. package/pages/projects/[id]/index.vue +182 -0
  62. package/pages/projects/[id]/languages.vue +537 -0
  63. package/pages/projects/[id]/review.vue +109 -0
  64. package/pages/projects/[id]/settings.vue +515 -0
  65. package/pages/projects/[id]/translations/[keyId].vue +642 -0
  66. package/pages/projects/[id]/translations/index.vue +250 -0
  67. package/pages/projects/[id]/users.vue +276 -0
  68. package/pages/projects/index.vue +334 -0
  69. package/pages/users/[id]/profile.vue +421 -0
  70. package/pages/users/index.vue +345 -0
  71. package/plugins/loading.client.ts +3 -0
  72. package/plugins/ui-i18n.ts +6 -0
  73. package/server/api/auth/login.post.ts +28 -0
  74. package/server/api/auth/logout.post.ts +7 -0
  75. package/server/api/auth/me.get.ts +11 -0
  76. package/server/api/auth/me.put.ts +31 -0
  77. package/server/api/auth/password.put.ts +27 -0
  78. package/server/api/auth/status.get.ts +16 -0
  79. package/server/api/config.get.ts +10 -0
  80. package/server/api/dashboard/layout.get.ts +18 -0
  81. package/server/api/dashboard/layout.post.ts +18 -0
  82. package/server/api/db-config.get.ts +44 -0
  83. package/server/api/db-config.post.ts +73 -0
  84. package/server/api/export.get.ts +64 -0
  85. package/server/api/formats/datetime/[id].delete.ts +8 -0
  86. package/server/api/formats/datetime/[id].put.ts +15 -0
  87. package/server/api/formats/datetime.get.ts +11 -0
  88. package/server/api/formats/datetime.post.ts +16 -0
  89. package/server/api/formats/modifiers/[id].delete.ts +8 -0
  90. package/server/api/formats/modifiers/[id].put.ts +10 -0
  91. package/server/api/formats/modifiers.get.ts +10 -0
  92. package/server/api/formats/modifiers.post.ts +14 -0
  93. package/server/api/formats/number/[id].delete.ts +8 -0
  94. package/server/api/formats/number/[id].put.ts +15 -0
  95. package/server/api/formats/number.get.ts +11 -0
  96. package/server/api/formats/number.post.ts +16 -0
  97. package/server/api/formats/snippet.get.ts +87 -0
  98. package/server/api/fs/browse.get.ts +50 -0
  99. package/server/api/history/[translationId].get.ts +13 -0
  100. package/server/api/keys/[id].delete.ts +14 -0
  101. package/server/api/keys/[id].get.ts +41 -0
  102. package/server/api/keys/[id].patch.ts +20 -0
  103. package/server/api/keys/index.get.ts +98 -0
  104. package/server/api/keys/index.post.ts +17 -0
  105. package/server/api/languages/[code].delete.ts +15 -0
  106. package/server/api/languages/[id].put.ts +24 -0
  107. package/server/api/languages/index.get.ts +13 -0
  108. package/server/api/languages/index.post.ts +42 -0
  109. package/server/api/onboarding.post.ts +56 -0
  110. package/server/api/profile.get.ts +81 -0
  111. package/server/api/project-snapshot.get.ts +73 -0
  112. package/server/api/project-snapshot.post.ts +160 -0
  113. package/server/api/projects/[id].delete.ts +13 -0
  114. package/server/api/projects/[id].put.ts +40 -0
  115. package/server/api/projects/index.get.ts +19 -0
  116. package/server/api/projects/index.post.ts +34 -0
  117. package/server/api/scan.post.ts +165 -0
  118. package/server/api/settings/index.get.ts +9 -0
  119. package/server/api/settings/index.post.ts +20 -0
  120. package/server/api/setup.post.ts +39 -0
  121. package/server/api/stats/global.get.ts +126 -0
  122. package/server/api/stats.get.ts +70 -0
  123. package/server/api/sync.post.ts +179 -0
  124. package/server/api/translate.post.ts +52 -0
  125. package/server/api/translations/batch-translate.post.ts +121 -0
  126. package/server/api/translations/bulk-status.post.ts +24 -0
  127. package/server/api/translations/index.post.ts +62 -0
  128. package/server/api/translations/job/[id].get.ts +23 -0
  129. package/server/api/translations/status.post.ts +30 -0
  130. package/server/api/translations/translate-all.post.ts +18 -0
  131. package/server/api/ui-locale.get.ts +39 -0
  132. package/server/api/users/[id]/profile.get.ts +107 -0
  133. package/server/api/users/[id]/roles.put.ts +67 -0
  134. package/server/api/users/[id].delete.ts +36 -0
  135. package/server/api/users/[id].put.ts +43 -0
  136. package/server/api/users/index.get.ts +49 -0
  137. package/server/api/users/index.post.ts +89 -0
  138. package/server/consts/auto-translate.const.ts +2 -0
  139. package/server/consts/commons.const.ts +10 -0
  140. package/server/consts/db.const.ts +3 -0
  141. package/server/consts/scanner.const.ts +4 -0
  142. package/server/consts/translation-job.const.ts +8 -0
  143. package/server/db/index.ts +672 -0
  144. package/server/enums/auth.enum.ts +5 -0
  145. package/server/enums/translation.enum.ts +6 -0
  146. package/server/interfaces/profile.interface.ts +48 -0
  147. package/server/interfaces/project-config.interface.ts +9 -0
  148. package/server/interfaces/scanner.interface.ts +18 -0
  149. package/server/interfaces/translation-job.interface.ts +13 -0
  150. package/server/middleware/auth.ts +32 -0
  151. package/server/plugins/db.ts +6 -0
  152. package/server/routes/locale/[lang].get.ts +179 -0
  153. package/server/types/auth.type.ts +3 -0
  154. package/server/utils/auth.util.ts +89 -0
  155. package/server/utils/auto-translate.util.ts +112 -0
  156. package/server/utils/lang-api.util.ts +24 -0
  157. package/server/utils/mailer.util.ts +80 -0
  158. package/server/utils/project-config.util.ts +37 -0
  159. package/server/utils/scanner.uti.ts +307 -0
  160. package/server/utils/translation-job.util.ts +142 -0
  161. package/services/auth.service.ts +31 -0
  162. package/services/base.service.ts +140 -0
  163. package/services/job.service.ts +10 -0
  164. package/services/key.service.ts +26 -0
  165. package/services/language.service.ts +26 -0
  166. package/services/profile.service.ts +14 -0
  167. package/services/project.service.ts +23 -0
  168. package/services/scan.service.ts +14 -0
  169. package/services/settings.service.ts +14 -0
  170. package/services/stats.service.ts +11 -0
  171. package/services/translation.service.ts +36 -0
  172. package/services/user.service.ts +28 -0
  173. package/tsconfig.json +3 -0
  174. package/types/commons.type.ts +3 -0
  175. package/types/dashboard.type.ts +26 -0
  176. package/utils/config.util.ts +60 -0
@@ -0,0 +1,563 @@
1
+ <template>
2
+ <div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
3
+ <div class="w-full max-w-2xl">
4
+ <!-- Header -->
5
+ <div class="text-center mb-8">
6
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 mb-4">
7
+ <UIcon name="i-heroicons-language" class="text-white text-3xl" />
8
+ </div>
9
+ <h1 class="text-3xl font-bold text-gray-900 dark:text-white">i18n Dashboard</h1>
10
+ <p class="text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.subtitle', 'Configurons votre espace de travail en quelques étapes') }}</p>
11
+ </div>
12
+
13
+ <!-- Steps indicator -->
14
+ <div class="flex items-center justify-center gap-2 mb-8">
15
+ <template v-for="(step, i) in steps" :key="i">
16
+ <div class="flex items-center gap-2">
17
+ <div
18
+ class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold transition-colors"
19
+ :class="i < currentStep
20
+ ? 'bg-primary-500 text-white'
21
+ : i === currentStep
22
+ ? 'bg-primary-500 text-white ring-4 ring-primary-100 dark:ring-primary-900/30'
23
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'"
24
+ >
25
+ <UIcon v-if="i < currentStep" name="i-heroicons-check" class="text-sm" />
26
+ <span v-else>{{ i + 1 }}</span>
27
+ </div>
28
+ <span class="text-sm hidden sm:block" :class="i === currentStep ? 'text-gray-900 dark:text-white font-medium' : 'text-gray-400'">{{ step.label }}</span>
29
+ </div>
30
+ <div v-if="i < steps.length - 1" class="flex-1 h-0.5 bg-gray-200 dark:bg-gray-700 max-w-12" />
31
+ </template>
32
+ </div>
33
+
34
+ <UCard>
35
+ <!-- ── Step 0 : Database ──────────────────────────────────────────── -->
36
+ <div v-if="currentStep === 0" class="space-y-4">
37
+ <div>
38
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.db_title', 'Database') }}</h2>
39
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
40
+ {{ t('onboarding.db_subtitle', 'Configure your database connection. Values are pre-filled from your config file.') }}
41
+ <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">i18n-dashboard.config.js</code>
42
+ </p>
43
+ </div>
44
+
45
+ <!-- DB type selector -->
46
+ <UFormField :label="t('onboarding.db_type_label', 'Database type')">
47
+ <USelect v-model="dbForm.type" :items="dbTypeOptions" class="w-full" />
48
+ </UFormField>
49
+
50
+ <!-- SQLite fields -->
51
+ <template v-if="dbForm.type === 'sqlite'">
52
+ <UFormField :label="t('onboarding.db_file_label', 'Database file')">
53
+ <div class="flex gap-2">
54
+ <UInput v-model="dbForm.connection" placeholder="./i18n-dashboard.db" class="flex-1" />
55
+ <UButton
56
+ v-if="!sqliteFileExists"
57
+ color="neutral"
58
+ variant="outline"
59
+ icon="i-heroicons-document-plus"
60
+ :loading="creatingFile"
61
+ @click="createSqliteFile"
62
+ >
63
+ {{ t('onboarding.db_create_file', 'Create') }}
64
+ </UButton>
65
+ <UBadge v-else color="success" variant="soft" class="shrink-0 self-center">
66
+ <UIcon name="i-heroicons-check" class="mr-1" />
67
+ {{ t('onboarding.db_file_found', 'File found') }}
68
+ </UBadge>
69
+ </div>
70
+ <p v-if="!sqliteFileExists" class="text-xs text-amber-500 mt-1">
71
+ <UIcon name="i-heroicons-exclamation-triangle" class="inline mr-1" />
72
+ {{ t('onboarding.db_file_missing', 'The file does not exist yet. Click "Create" to create it.') }}
73
+ </p>
74
+ </UFormField>
75
+ </template>
76
+
77
+ <!-- PostgreSQL / MySQL fields -->
78
+ <template v-else>
79
+ <div class="grid grid-cols-2 gap-3">
80
+ <UFormField :label="t('onboarding.db_host', 'Host')" class="col-span-1">
81
+ <UInput v-model="dbForm.host" placeholder="localhost" class="w-full" />
82
+ </UFormField>
83
+ <UFormField :label="t('onboarding.db_port', 'Port')" class="col-span-1">
84
+ <UInput v-model="dbForm.port" :placeholder="dbForm.type === 'mysql' ? '3306' : '5432'" class="w-full" />
85
+ </UFormField>
86
+ </div>
87
+ <UFormField :label="t('onboarding.db_user', 'User')">
88
+ <UInput v-model="dbForm.user" placeholder="postgres" class="w-full" />
89
+ </UFormField>
90
+ <UFormField :label="t('onboarding.db_password', 'Password')">
91
+ <UInput v-model="dbForm.password" type="password" placeholder="••••••••" class="w-full" />
92
+ </UFormField>
93
+ <UFormField :label="t('onboarding.db_name', 'Database name')">
94
+ <UInput v-model="dbForm.database" placeholder="i18n_dashboard" class="w-full" />
95
+ </UFormField>
96
+ </template>
97
+
98
+ <!-- Actions -->
99
+ <div class="flex items-center gap-3 flex-wrap">
100
+ <UButton
101
+ color="neutral"
102
+ variant="outline"
103
+ icon="i-heroicons-signal"
104
+ :loading="testingDb"
105
+ @click="testDbConnection"
106
+ >
107
+ {{ t('onboarding.db_test', 'Test connection') }}
108
+ </UButton>
109
+ <UButton
110
+ v-if="dbFormChanged"
111
+ icon="i-heroicons-check"
112
+ :loading="applyingDb"
113
+ @click="applyDbConfig"
114
+ >
115
+ {{ t('onboarding.db_test_apply', 'Apply') }}
116
+ </UButton>
117
+ <UBadge v-if="dbConnected" color="success" variant="soft">
118
+ <UIcon name="i-heroicons-check-circle" class="mr-1" />
119
+ {{ t('onboarding.db_connected', 'Connection OK') }}
120
+ </UBadge>
121
+ </div>
122
+
123
+ <p v-if="dbError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">
124
+ <UIcon name="i-heroicons-exclamation-circle" class="inline mr-1" />
125
+ {{ dbError }}
126
+ </p>
127
+ </div>
128
+
129
+ <!-- ── Step 1 : Admin account ─────────────────────────────────────── -->
130
+ <div v-if="currentStep === 1" class="space-y-4">
131
+ <!-- Fresh install: creation form -->
132
+ <template v-if="!hasUsers">
133
+ <div class="bg-primary-50 dark:bg-primary-900/20 rounded-lg p-4">
134
+ <p class="text-sm text-primary-700 dark:text-primary-300">
135
+ <UIcon name="i-heroicons-information-circle" class="inline mr-1" />
136
+ {{ t('onboarding.create_admin_hint', 'Create the') }} <strong>{{ t('onboarding.super_admin_role', 'Super Administrator') }}</strong> {{ t('onboarding.create_admin_hint2', 'account to get started.') }}
137
+ </p>
138
+ </div>
139
+ <form class="space-y-4" @submit.prevent="createAdmin">
140
+ <UFormField :label="t('users.full_name', 'Full name')" required>
141
+ <UInput v-model="adminForm.name" placeholder="Marie Dupont" class="w-full" autofocus />
142
+ </UFormField>
143
+ <UFormField :label="t('login.email', 'Email')" required>
144
+ <UInput v-model="adminForm.email" type="email" placeholder="admin@example.com" class="w-full" />
145
+ </UFormField>
146
+ <UFormField :label="t('login.password', 'Password')" :hint="t('user.password_hint', 'Minimum 8 characters')" required>
147
+ <UInput v-model="adminForm.password" type="password" placeholder="••••••••" class="w-full" />
148
+ </UFormField>
149
+ <UFormField :label="t('onboarding.confirm_password', 'Confirm password')" required>
150
+ <UInput v-model="adminForm.confirm" type="password" placeholder="••••••••" class="w-full" />
151
+ </UFormField>
152
+ <p v-if="adminError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">
153
+ <UIcon name="i-heroicons-exclamation-circle" class="inline mr-1" />
154
+ {{ adminError }}
155
+ </p>
156
+ </form>
157
+ </template>
158
+ <!-- Existing install: confirmation -->
159
+ <template v-else>
160
+ <div class="flex items-center gap-3 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
161
+ <UIcon name="i-heroicons-check-circle" class="text-green-500 text-2xl shrink-0" />
162
+ <div>
163
+ <p class="font-medium text-green-700 dark:text-green-400">{{ t('onboarding.admin_done', 'Compte administrateur créé avec succès.') }}</p>
164
+ <p class="text-sm text-green-600 dark:text-green-500 mt-0.5">{{ currentUser?.name }} — {{ currentUser?.email }}</p>
165
+ </div>
166
+ </div>
167
+ </template>
168
+ </div>
169
+
170
+ <!-- ── Step 2 : UI Languages ──────────────────────────────────────── -->
171
+ <div v-if="currentStep === 2" class="space-y-4">
172
+ <div>
173
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.languages_title', 'Langues de l\'interface') }}</h2>
174
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.languages_hint', 'Sélectionnez les langues disponibles pour l\'interface du dashboard.') }}</p>
175
+ </div>
176
+ <UInput
177
+ v-model="uiLangSearch"
178
+ :placeholder="t('onboarding.languages_search', 'Rechercher une langue...')"
179
+ icon="i-heroicons-magnifying-glass"
180
+ class="w-full"
181
+ />
182
+ <div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden max-h-56 overflow-y-auto">
183
+ <button
184
+ v-for="lang in filteredUiLangs"
185
+ :key="lang.code"
186
+ class="w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
187
+ :class="selectedUiLangs.includes(lang.code) ? 'bg-primary-50 dark:bg-primary-900/20' : ''"
188
+ @click="toggleUiLang(lang)"
189
+ >
190
+ <span class="font-mono text-xs text-gray-400 w-10 shrink-0">{{ lang.code }}</span>
191
+ <span class="flex-1 text-gray-700 dark:text-gray-300">{{ lang.nativeName }}</span>
192
+ <span class="text-xs text-gray-400">{{ lang.name }}</span>
193
+ <UIcon v-if="selectedUiLangs.includes(lang.code)" name="i-heroicons-check" class="text-primary-500 shrink-0" />
194
+ </button>
195
+ </div>
196
+ <p class="text-xs text-gray-400">
197
+ {{ selectedUiLangs.length }} {{ t('onboarding.languages_selected', 'langue(s) sélectionnée(s)') }}
198
+ <span v-if="nonEnLangs.length" class="ml-1 text-primary-500">
199
+ — {{ nonEnLangs.length }} {{ t('onboarding.langs_will_be_translated', 'language(s) will be automatically translated via Google Translate') }}
200
+ </span>
201
+ </p>
202
+ <UFormField :label="t('onboarding.languages_default', 'Langue par défaut')">
203
+ <USelect v-model="defaultUiLang" :items="selectedUiLangsOptions" class="w-full" />
204
+ </UFormField>
205
+ </div>
206
+
207
+ <!-- ── Step 3 : First project ─────────────────────────────────────── -->
208
+ <div v-if="currentStep === 3" class="space-y-4">
209
+ <div>
210
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.project_title', 'Votre premier projet') }}</h2>
211
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.project_hint', 'Configurez le projet Vue.js que vous souhaitez gérer.') }}</p>
212
+ </div>
213
+ <UFormField :label="t('projects.name_label', 'Project name')" required>
214
+ <UInput v-model="projectForm.name" :placeholder="t('projects.name_placeholder', 'My App')" class="w-full" />
215
+ </UFormField>
216
+ <UFormField :label="t('settings.root_path', 'Project path')" required>
217
+ <UInput v-model="projectForm.root_path" placeholder="/path/to/my-project" class="w-full" />
218
+ </UFormField>
219
+ <UFormField :label="t('settings.locales_folder', 'Locales folder')">
220
+ <UInput v-model="projectForm.locales_path" placeholder="src/locales" class="w-full" />
221
+ </UFormField>
222
+ <p v-if="projectError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">{{ projectError }}</p>
223
+ </div>
224
+
225
+ <!-- ── Step 4 : Done ──────────────────────────────────────────────── -->
226
+ <div v-if="currentStep === 4" class="text-center space-y-4 py-4">
227
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-900/30 mb-2">
228
+ <UIcon name="i-heroicons-check" class="text-green-500 text-3xl" />
229
+ </div>
230
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('onboarding.done_title', 'Tout est prêt !') }}</h2>
231
+ <p class="text-gray-500 dark:text-gray-400">{{ t('onboarding.done_hint', 'Votre dashboard est configuré. Vous pouvez maintenant gérer vos traductions.') }}</p>
232
+ <p v-if="nonEnLangs.length" class="text-sm text-primary-500">
233
+ {{ t('onboarding.translating_in_progress', 'Interface translation is in progress in the background for') }} {{ nonEnLangs.join(', ') }}.
234
+ </p>
235
+ </div>
236
+
237
+ <!-- ── Footer navigation ──────────────────────────────────────────── -->
238
+ <template #footer>
239
+ <div class="flex items-center justify-between">
240
+ <UButton
241
+ v-if="currentStep > 0 && currentStep < 4"
242
+ color="neutral"
243
+ variant="ghost"
244
+ icon="i-heroicons-arrow-left"
245
+ @click="currentStep--"
246
+ >
247
+ {{ t('onboarding.previous', 'Précédent') }}
248
+ </UButton>
249
+ <div v-else />
250
+
251
+ <div class="flex items-center gap-3">
252
+ <UButton
253
+ v-if="currentStep === 3"
254
+ color="neutral"
255
+ variant="ghost"
256
+ @click="skipProject"
257
+ >
258
+ {{ t('onboarding.project_skip', 'Passer cette étape') }}
259
+ </UButton>
260
+
261
+ <!-- Step 0: DB -->
262
+ <UButton v-if="currentStep === 0" icon="i-heroicons-arrow-right" trailing @click="currentStep++">
263
+ {{ t('onboarding.next', 'Suivant') }}
264
+ </UButton>
265
+
266
+ <!-- Step 1: Admin -->
267
+ <UButton
268
+ v-if="currentStep === 1"
269
+ :loading="saving"
270
+ icon="i-heroicons-arrow-right"
271
+ trailing
272
+ @click="hasUsers ? currentStep++ : createAdmin()"
273
+ >
274
+ {{ t('onboarding.next', 'Suivant') }}
275
+ </UButton>
276
+
277
+ <!-- Step 2: Languages -->
278
+ <UButton
279
+ v-if="currentStep === 2"
280
+ :loading="saving"
281
+ :disabled="selectedUiLangs.length === 0"
282
+ icon="i-heroicons-arrow-right"
283
+ trailing
284
+ @click="saveLanguages"
285
+ >
286
+ {{ t('onboarding.next', 'Suivant') }}
287
+ </UButton>
288
+
289
+ <!-- Step 3: Project -->
290
+ <UButton
291
+ v-if="currentStep === 3"
292
+ :loading="saving"
293
+ icon="i-heroicons-arrow-right"
294
+ trailing
295
+ @click="saveProject"
296
+ >
297
+ {{ t('onboarding.finish', 'Terminer') }}
298
+ </UButton>
299
+
300
+ <!-- Step 4: Done -->
301
+ <UButton
302
+ v-if="currentStep === 4"
303
+ icon="i-heroicons-home"
304
+ trailing
305
+ @click="goToDashboard"
306
+ >
307
+ {{ t('onboarding.go_to_dashboard', 'Aller au dashboard') }}
308
+ </UButton>
309
+ </div>
310
+ </div>
311
+ </template>
312
+ </UCard>
313
+ </div>
314
+ </div>
315
+ </template>
316
+
317
+ <script lang="ts" setup>
318
+ definePageMeta({ layout: false })
319
+
320
+ const router = useRouter()
321
+ const { t } = useT()
322
+ const { currentUser, fetchMe } = useAuth()
323
+ await fetchMe()
324
+
325
+ const { data: authStatus } = await useFetch('/api/auth/status', { key: 'auth-status' })
326
+ const hasUsers = computed(() => !!(authStatus.value as any)?.hasUsers)
327
+
328
+ const currentStep = ref(0)
329
+ const saving = ref(false)
330
+
331
+ const steps = [
332
+ { label: t('onboarding.step_db', 'Database') },
333
+ { label: t('onboarding.step_admin', 'Admin account') },
334
+ { label: t('onboarding.step_languages', 'Interface languages') },
335
+ { label: t('onboarding.step_project', 'First project') },
336
+ { label: t('onboarding.step_done', 'Done') },
337
+ ]
338
+
339
+ // ─── Step 0 : Database config ──────────────────────────────────────────────────
340
+ const { data: dbConfig, refresh: refreshDbConfig } = await useFetch<any>('/api/db-config')
341
+
342
+ const dbTypeOptions = computed(() => [
343
+ { label: t('onboarding.db_type_sqlite', 'SQLite (local file)'), value: 'sqlite' },
344
+ { label: t('onboarding.db_type_postgresql', 'PostgreSQL'), value: 'postgresql' },
345
+ { label: t('onboarding.db_type_mysql', 'MySQL / MariaDB'), value: 'mysql' },
346
+ ])
347
+
348
+ const dbForm = ref({
349
+ type: (dbConfig.value?.type as string) || 'sqlite',
350
+ connection: (dbConfig.value?.connection as string) || './i18n-dashboard.db',
351
+ host: (dbConfig.value?.host as string) || 'localhost',
352
+ port: (dbConfig.value?.port as string) || '5432',
353
+ user: (dbConfig.value?.user as string) || '',
354
+ password: '',
355
+ database: (dbConfig.value?.database as string) || '',
356
+ })
357
+
358
+ const sqliteFileExists = ref<boolean>(dbConfig.value?.fileExists ?? true)
359
+ const dbConnected = ref(false)
360
+ const testingDb = ref(false)
361
+ const applyingDb = ref(false)
362
+ const creatingFile = ref(false)
363
+ const dbError = ref('')
364
+
365
+ // Track if form changed from initial config
366
+ const dbFormOriginal = JSON.stringify(dbForm.value)
367
+ const dbFormChanged = computed(() => JSON.stringify(dbForm.value) !== dbFormOriginal)
368
+
369
+ // Update sqliteFileExists when SQLite path changes
370
+ let _checkPathTimer: ReturnType<typeof setTimeout> | null = null
371
+ watch(() => dbForm.value.connection, (path) => {
372
+ if (dbForm.value.type !== 'sqlite') return
373
+ if (_checkPathTimer) clearTimeout(_checkPathTimer)
374
+ _checkPathTimer = setTimeout(async () => {
375
+ try {
376
+ const result = await $fetch<any>(`/api/db-config?checkPath=${encodeURIComponent(path)}`)
377
+ sqliteFileExists.value = result.fileExists ?? false
378
+ } catch { /* ignore */ }
379
+ }, 400)
380
+ })
381
+
382
+ async function createSqliteFile() {
383
+ creatingFile.value = true
384
+ dbError.value = ''
385
+ try {
386
+ await $fetch('/api/db-config', {
387
+ method: 'POST',
388
+ body: { type: 'sqlite', connection: dbForm.value.connection, createFile: true },
389
+ })
390
+ sqliteFileExists.value = true
391
+ } catch (e: any) {
392
+ dbError.value = e.data?.message || t('onboarding.db_create_file_error', 'Error creating the file.')
393
+ } finally {
394
+ creatingFile.value = false
395
+ }
396
+ }
397
+
398
+ function dbBody() {
399
+ return {
400
+ type: dbForm.value.type,
401
+ connection: dbForm.value.connection,
402
+ host: dbForm.value.host,
403
+ port: dbForm.value.port,
404
+ user: dbForm.value.user,
405
+ password: dbForm.value.password,
406
+ database: dbForm.value.database,
407
+ }
408
+ }
409
+
410
+ async function testDbConnection() {
411
+ testingDb.value = true
412
+ dbConnected.value = false
413
+ dbError.value = ''
414
+ try {
415
+ await $fetch('/api/db-config', { method: 'POST', body: { ...dbBody(), testOnly: true } })
416
+ dbConnected.value = true
417
+ } catch (e: any) {
418
+ dbError.value = e.data?.message || 'Connection failed.'
419
+ } finally {
420
+ testingDb.value = false
421
+ }
422
+ }
423
+
424
+ async function applyDbConfig() {
425
+ applyingDb.value = true
426
+ dbError.value = ''
427
+ try {
428
+ await $fetch('/api/db-config', { method: 'POST', body: dbBody() })
429
+ dbConnected.value = true
430
+ await refreshDbConfig()
431
+ sqliteFileExists.value = dbConfig.value?.fileExists ?? true
432
+ } catch (e: any) {
433
+ dbError.value = e.data?.message || 'Failed to apply configuration.'
434
+ dbConnected.value = false
435
+ } finally {
436
+ applyingDb.value = false
437
+ }
438
+ }
439
+
440
+
441
+ // ─── Step 1 : Admin account ────────────────────────────────────────────────────
442
+ const adminForm = ref({ name: '', email: '', password: '', confirm: '' })
443
+ const adminError = ref('')
444
+
445
+ async function createAdmin() {
446
+ adminError.value = ''
447
+ if (!adminForm.value.name || !adminForm.value.email || !adminForm.value.password) {
448
+ adminError.value = t('onboarding.all_fields_required', 'All fields are required.')
449
+ return
450
+ }
451
+ if (adminForm.value.password.length < 8) {
452
+ adminError.value = t('onboarding.password_min_length', 'Password must be at least 8 characters.')
453
+ return
454
+ }
455
+ if (adminForm.value.password !== adminForm.value.confirm) {
456
+ adminError.value = t('onboarding.passwords_mismatch', 'Passwords do not match.')
457
+ return
458
+ }
459
+ saving.value = true
460
+ try {
461
+ await $fetch('/api/setup', {
462
+ method: 'POST',
463
+ body: { name: adminForm.value.name, email: adminForm.value.email, password: adminForm.value.password },
464
+ })
465
+ await fetchMe()
466
+ currentStep.value = 2
467
+ } catch (e: any) {
468
+ adminError.value = e.data?.message || t('onboarding.admin_creation_error', 'Error creating the account.')
469
+ } finally {
470
+ saving.value = false
471
+ }
472
+ }
473
+
474
+ // ─── Step 2 : UI Languages ─────────────────────────────────────────────────────
475
+ const { languages: allWorldLangs, filteredLanguages, searchQuery: uiLangSearch } = useLanguages()
476
+
477
+ const { data: configData } = await useFetch<{ uiLanguages?: string[]; defaultUiLanguage?: string }>('/api/config')
478
+ const selectedUiLangs = ref<string[]>(configData.value?.uiLanguages || ['en'])
479
+ const defaultUiLang = ref(configData.value?.defaultUiLanguage || 'en')
480
+
481
+ const filteredUiLangs = computed(() => filteredLanguages.value)
482
+ const nonEnLangs = computed(() => selectedUiLangs.value.filter(c => c !== 'en'))
483
+
484
+ const selectedUiLangsOptions = computed(() =>
485
+ selectedUiLangs.value.map((code) => {
486
+ const lang = allWorldLangs.find((l) => l.code === code)
487
+ return { label: lang ? `${lang.nativeName} (${code})` : code, value: code }
488
+ }),
489
+ )
490
+
491
+ function toggleUiLang(lang: { code: string; name: string; nativeName: string }) {
492
+ const idx = selectedUiLangs.value.indexOf(lang.code)
493
+ if (idx >= 0) {
494
+ if (selectedUiLangs.value.length === 1) return
495
+ selectedUiLangs.value.splice(idx, 1)
496
+ if (defaultUiLang.value === lang.code) defaultUiLang.value = selectedUiLangs.value[0]
497
+ } else {
498
+ selectedUiLangs.value.push(lang.code)
499
+ if (selectedUiLangs.value.length === 1) defaultUiLang.value = lang.code
500
+ }
501
+ }
502
+
503
+ async function saveLanguages() {
504
+ saving.value = true
505
+ try {
506
+ const langs = selectedUiLangs.value.map((code) => {
507
+ const lang = allWorldLangs.find((l) => l.code === code)
508
+ return { code, name: lang?.name || code }
509
+ })
510
+ await $fetch('/api/onboarding', {
511
+ method: 'POST',
512
+ body: { languages: langs, defaultLanguage: defaultUiLang.value },
513
+ })
514
+ currentStep.value = 3
515
+ } catch {
516
+ // silent — onboarding marked complete regardless
517
+ } finally {
518
+ saving.value = false
519
+ }
520
+ }
521
+
522
+ // ─── Step 3 : First project ────────────────────────────────────────────────────
523
+ const projectForm = ref({
524
+ name: configData.value?.project?.name || '',
525
+ root_path: '',
526
+ locales_path: configData.value?.project?.localesPath || 'src/locales',
527
+ })
528
+ const projectError = ref('')
529
+
530
+ async function saveProject() {
531
+ projectError.value = ''
532
+ if (!projectForm.value.name.trim() || !projectForm.value.root_path.trim()) {
533
+ projectError.value = t('onboarding.project_name_path_required', 'Project name and path are required.')
534
+ return
535
+ }
536
+ saving.value = true
537
+ try {
538
+ await $fetch('/api/projects', {
539
+ method: 'POST',
540
+ body: {
541
+ name: projectForm.value.name.trim(),
542
+ root_path: projectForm.value.root_path.trim(),
543
+ locales_path: projectForm.value.locales_path || 'src/locales',
544
+ },
545
+ })
546
+ currentStep.value = 4
547
+ } catch (e: any) {
548
+ projectError.value = e.data?.message || t('onboarding.project_creation_error', 'Error creating the project.')
549
+ } finally {
550
+ saving.value = false
551
+ }
552
+ }
553
+
554
+ function skipProject() {
555
+ currentStep.value = 4
556
+ }
557
+
558
+ // ─── Step 4 : Done ─────────────────────────────────────────────────────────────
559
+ async function goToDashboard() {
560
+ await clearNuxtData('auth-status')
561
+ await router.push('/')
562
+ }
563
+ </script>