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,231 @@
1
+ import { keyService } from '../services/key.service'
2
+ import { translationService } from '../services/translation.service'
3
+ import { scanService } from '../services/scan.service'
4
+ import type { KeysQuery, KeysResponse } from '../services/key.service'
5
+
6
+ export function useKeys(options: {
7
+ queryParams?: Ref<KeysQuery>
8
+ id?: Ref<string | string[]>
9
+ } = {}) {
10
+ const toast = useToast()
11
+ const router = useRouter()
12
+ const route = useRoute()
13
+
14
+ // ── List mode ─────────────────────────────────────────────────────────────
15
+
16
+ const data = ref<KeysResponse | null>(null)
17
+ const listPending = ref(false)
18
+
19
+ async function refresh() {
20
+ if (!options.queryParams?.value.project_id) return
21
+ listPending.value = true
22
+ try {
23
+ data.value = await keyService.getKeys(options.queryParams.value)
24
+ }
25
+ catch {}
26
+ finally {
27
+ listPending.value = false
28
+ }
29
+ }
30
+
31
+ if (options.queryParams) {
32
+ watch(options.queryParams, refresh, { deep: true, immediate: true })
33
+ }
34
+
35
+ const addingKey = ref(false)
36
+ async function createKey(projectId: number, key: string, description?: string): Promise<boolean> {
37
+ addingKey.value = true
38
+ try {
39
+ await keyService.createKey({ project_id: projectId, key, description })
40
+ toast.add({ title: 'Clé créée', color: 'success' })
41
+ await refresh()
42
+ refreshNuxtData('project-stats')
43
+ return true
44
+ }
45
+ catch {
46
+ return false
47
+ }
48
+ finally {
49
+ addingKey.value = false
50
+ }
51
+ }
52
+
53
+ const scanning = ref(false)
54
+ async function scan(projectId: number): Promise<void> {
55
+ scanning.value = true
56
+ try {
57
+ const result = await scanService.scan(projectId)
58
+ const langMsg = result.langsAdded > 0 ? ` · ${result.langsAdded} langue(s) ajoutée(s)` : ''
59
+ toast.add({
60
+ title: 'Scan terminé',
61
+ description: `${result.keysFound} clés trouvées · ${result.keysAdded} nouvelles${langMsg}`,
62
+ color: 'success',
63
+ })
64
+ await refresh()
65
+ }
66
+ catch {}
67
+ finally {
68
+ scanning.value = false
69
+ }
70
+ }
71
+
72
+ const syncing = ref(false)
73
+ async function sync(projectId: number): Promise<void> {
74
+ syncing.value = true
75
+ try {
76
+ const result = await scanService.sync(projectId)
77
+ toast.add({
78
+ title: 'Sync terminée',
79
+ description: `${result.added} ajoutées · ${result.updated} mises à jour`,
80
+ color: 'success',
81
+ })
82
+ await refresh()
83
+ }
84
+ catch {}
85
+ finally {
86
+ syncing.value = false
87
+ }
88
+ }
89
+
90
+ const batchTranslating = ref(false)
91
+ async function batchTranslate(projectId: number, targetLang: string): Promise<void> {
92
+ batchTranslating.value = true
93
+ try {
94
+ const result = await translationService.batchTranslate(projectId, targetLang)
95
+ toast.add({
96
+ title: 'Traduction automatique terminée',
97
+ description: `${result.translated} traduites · ${result.skipped} ignorées · ${result.errors} erreurs`,
98
+ color: 'success',
99
+ })
100
+ await refresh()
101
+ }
102
+ catch {}
103
+ finally {
104
+ batchTranslating.value = false
105
+ }
106
+ }
107
+
108
+ // ── Detail mode ───────────────────────────────────────────────────────────
109
+
110
+ const keyId = computed(() => {
111
+ if (!options.id) return null
112
+ const v = options.id.value
113
+ return Array.isArray(v) ? v[0] : v
114
+ })
115
+
116
+ const { data: keyData, pending: detailPending, refresh: detailRefresh } = useAsyncData(
117
+ `key-${keyId.value ?? 'none'}`,
118
+ () => keyId.value ? keyService.getKey(keyId.value) : Promise.resolve(null),
119
+ { watch: [keyId] },
120
+ )
121
+
122
+ const savingLang = ref<string | null>(null)
123
+ async function saveTranslation(langCode: string, value: string): Promise<void> {
124
+ if (!keyData.value) return
125
+ savingLang.value = langCode
126
+ try {
127
+ await translationService.save({ key_id: keyData.value.id, language_code: langCode, value })
128
+ await detailRefresh()
129
+ }
130
+ catch {}
131
+ finally {
132
+ savingLang.value = null
133
+ }
134
+ }
135
+
136
+ const settingStatus = ref<string | null>(null)
137
+ async function setStatus(langCode: string, status: string): Promise<void> {
138
+ if (!keyData.value) return
139
+ settingStatus.value = `${langCode}:${status}`
140
+ try {
141
+ await translationService.setStatus({ key_id: keyData.value.id, language_code: langCode, status })
142
+ await detailRefresh()
143
+ }
144
+ catch {}
145
+ finally {
146
+ settingStatus.value = null
147
+ }
148
+ }
149
+
150
+ async function restoreVersion(langCode: string, value: string): Promise<void> {
151
+ if (!keyData.value) return
152
+ try {
153
+ await translationService.save({ key_id: keyData.value.id, language_code: langCode, value })
154
+ await detailRefresh()
155
+ toast.add({ title: 'Version restaurée', color: 'success' })
156
+ }
157
+ catch {}
158
+ }
159
+
160
+ async function autoTranslate(langCode: string, text: string, sourceLang: string): Promise<string | null> {
161
+ try {
162
+ const result = await translationService.translateText(text, sourceLang, langCode)
163
+ return result.text
164
+ }
165
+ catch {
166
+ return null
167
+ }
168
+ }
169
+
170
+ const savingDescription = ref(false)
171
+ async function updateDescription(description: string | null): Promise<void> {
172
+ if (!keyData.value) return
173
+ savingDescription.value = true
174
+ try {
175
+ await keyService.updateKey(keyData.value.id, { description })
176
+ await detailRefresh()
177
+ toast.add({ title: 'Description mise à jour', color: 'success' })
178
+ }
179
+ catch {}
180
+ finally {
181
+ savingDescription.value = false
182
+ }
183
+ }
184
+
185
+ const deleting = ref(false)
186
+ async function deleteKey(): Promise<void> {
187
+ if (!keyData.value) return
188
+ deleting.value = true
189
+ try {
190
+ await keyService.deleteKey(keyData.value.id)
191
+ toast.add({ title: 'Clé supprimée', color: 'success' })
192
+ const projectId = route.params.id
193
+ router.push(projectId ? `/projects/${projectId}/translations` : '/projects')
194
+ }
195
+ catch {
196
+ deleting.value = false
197
+ }
198
+ }
199
+
200
+ // ── Shared ────────────────────────────────────────────────────────────────
201
+
202
+ const pending = computed(() => listPending.value || detailPending.value)
203
+
204
+ return {
205
+ // List
206
+ data,
207
+ addingKey,
208
+ createKey,
209
+ scanning,
210
+ scan,
211
+ syncing,
212
+ sync,
213
+ batchTranslating,
214
+ batchTranslate,
215
+ // Detail
216
+ keyData,
217
+ savingLang,
218
+ saveTranslation,
219
+ settingStatus,
220
+ setStatus,
221
+ restoreVersion,
222
+ autoTranslate,
223
+ savingDescription,
224
+ updateDescription,
225
+ deleting,
226
+ deleteKey,
227
+ // Shared
228
+ pending,
229
+ refresh: options.id ? detailRefresh : refresh,
230
+ }
231
+ }
@@ -0,0 +1,221 @@
1
+ import { LANGUAGES } from '../consts/languages.const'
2
+ import { languageService } from '../services/language.service'
3
+ import { translationService } from '../services/translation.service'
4
+ import { jobService } from '../services/job.service'
5
+ import type { Language, LanguageItem, CreateLanguagePayload } from '../interfaces/languages.interface'
6
+
7
+ // ── Static language lookup ───────────────────────────────────────────────────
8
+
9
+ export function useLanguages() {
10
+ const toast = useToast()
11
+ const { currentProject } = useProject()
12
+
13
+ // ── Static lookup ────────────────────────────────────────────────────────
14
+
15
+ const searchQuery = ref('')
16
+
17
+ const filteredLanguages = computed(() => {
18
+ const q = searchQuery.value.toLowerCase()
19
+ if (!q) return LANGUAGES
20
+ return LANGUAGES.filter(
21
+ l => l.code.toLowerCase().includes(q)
22
+ || l.name.toLowerCase().includes(q)
23
+ || l.nativeName.toLowerCase().includes(q),
24
+ )
25
+ })
26
+
27
+ function findLanguage(code: string): Language | undefined {
28
+ // Exact match first, then fall back to base language (fr-CA → fr)
29
+ return LANGUAGES.find(l => l.code === code)
30
+ ?? LANGUAGES.find(l => l.code === code.split('-')[0])
31
+ }
32
+
33
+ function getLanguageName(code: string): string {
34
+ const lang = findLanguage(code)
35
+ return lang ? `${lang.nativeName} (${lang.name})` : code
36
+ }
37
+
38
+ // ── Project languages (API) ──────────────────────────────────────────────
39
+
40
+ const { data, pending, refresh } = useAsyncData<LanguageItem[]>(
41
+ 'project-languages',
42
+ () => languageService.getLanguages(currentProject.value?.id),
43
+ { watch: [() => currentProject.value?.id], default: () => [] },
44
+ )
45
+
46
+ const projectLanguages = computed(() => data.value ?? [])
47
+
48
+ const adding = ref(false)
49
+ async function addLanguage(payload: Omit<CreateLanguagePayload, 'project_id'>): Promise<void> {
50
+ if (!currentProject.value) return
51
+ adding.value = true
52
+ try {
53
+ await languageService.create({ ...payload, project_id: currentProject.value.id })
54
+ await refresh()
55
+ refreshNuxtData('project-stats')
56
+ }
57
+ finally {
58
+ adding.value = false
59
+ }
60
+ }
61
+
62
+ const deleting = ref(false)
63
+ async function deleteLanguage(code: string): Promise<void> {
64
+ if (!currentProject.value) return
65
+ deleting.value = true
66
+ try {
67
+ await languageService.remove(code, currentProject.value.id)
68
+ await refresh()
69
+ refreshNuxtData('project-stats')
70
+ toast.add({ title: 'Langue supprimée', color: 'success' })
71
+ }
72
+ finally {
73
+ deleting.value = false
74
+ }
75
+ }
76
+
77
+ async function setDefault(lang: LanguageItem): Promise<void> {
78
+ if (!currentProject.value) return
79
+ try {
80
+ await languageService.setDefault(lang, currentProject.value.id)
81
+ await refresh()
82
+ }
83
+ catch {}
84
+ }
85
+
86
+ async function setFallback(lang: LanguageItem, fallbackCode: string | null): Promise<void> {
87
+ await $fetch(`/api/languages/${lang.id}`, {
88
+ method: 'PUT',
89
+ body: { fallback_code: fallbackCode },
90
+ })
91
+ await refresh()
92
+ }
93
+
94
+ async function startTranslateAll(languageCode: string, languageName: string): Promise<string | null> {
95
+ if (!currentProject.value) return null
96
+ try {
97
+ const job = await translationService.translateAll(currentProject.value.id, languageCode, languageName)
98
+ return job.jobId
99
+ }
100
+ catch {
101
+ return null
102
+ }
103
+ }
104
+
105
+ // ── Translation job polling ──────────────────────────────────────────────
106
+
107
+ const showProgress = ref(false)
108
+ const progressJobId = ref<string | null>(null)
109
+ const progressLangName = ref('')
110
+ const progressTotal = ref(0)
111
+ const progressDone = ref(0)
112
+ const progressPercent = ref(0)
113
+ const progressStatus = ref<'running' | 'done' | 'error'>('running')
114
+
115
+ let _pollInterval: ReturnType<typeof setInterval> | null = null
116
+
117
+ async function _pollJob() {
118
+ if (!progressJobId.value) return
119
+ try {
120
+ const job = await jobService.getJob(progressJobId.value)
121
+ progressTotal.value = job.total
122
+ progressDone.value = job.done
123
+ progressPercent.value = job.percent
124
+ progressStatus.value = job.status
125
+ if (job.status !== 'running') _stopPolling()
126
+ }
127
+ catch {
128
+ _stopPolling()
129
+ }
130
+ }
131
+
132
+ function _stopPolling() {
133
+ if (_pollInterval) { clearInterval(_pollInterval); _pollInterval = null }
134
+ }
135
+
136
+ function startPolling(jobId: string, langName: string) {
137
+ progressJobId.value = jobId
138
+ progressLangName.value = langName
139
+ progressTotal.value = 0
140
+ progressDone.value = 0
141
+ progressPercent.value = 0
142
+ progressStatus.value = 'running'
143
+ showProgress.value = true
144
+ _stopPolling()
145
+ _pollInterval = setInterval(_pollJob, 800)
146
+ }
147
+
148
+ function closeProgress() {
149
+ showProgress.value = false
150
+ _stopPolling()
151
+ progressJobId.value = null
152
+ }
153
+
154
+ function sendToBackground(onDone?: () => void) {
155
+ showProgress.value = false
156
+ const langName = progressLangName.value
157
+
158
+ const toastRef = toast.add({
159
+ title: `Traduction ${langName} en cours…`,
160
+ description: `${progressPercent.value}% — ${progressDone.value} / ${progressTotal.value} clés`,
161
+ duration: 0,
162
+ color: 'info',
163
+ })
164
+
165
+ const bgInterval = setInterval(async () => {
166
+ if (!progressJobId.value) { clearInterval(bgInterval); return }
167
+ try {
168
+ const job = await jobService.getJob(progressJobId.value)
169
+ progressTotal.value = job.total
170
+ progressDone.value = job.done
171
+ progressPercent.value = job.percent
172
+ progressStatus.value = job.status
173
+
174
+ if (job.status !== 'running') {
175
+ clearInterval(bgInterval)
176
+ _stopPolling()
177
+ toast.remove(toastRef?.id ?? '')
178
+ toast.add({
179
+ title: job.errors ? `Traduction ${langName} terminée avec erreurs` : `Traduction ${langName} terminée`,
180
+ description: `${job.done} clés traduites${job.errors ? ` · ${job.errors} erreurs` : ''}`,
181
+ color: job.errors ? 'warning' : 'success',
182
+ })
183
+ onDone?.()
184
+ }
185
+ }
186
+ catch { clearInterval(bgInterval) }
187
+ }, 800)
188
+ }
189
+
190
+ onUnmounted(_stopPolling)
191
+
192
+ return {
193
+ // Static lookup
194
+ languages: LANGUAGES,
195
+ filteredLanguages,
196
+ searchQuery,
197
+ findLanguage,
198
+ getLanguageName,
199
+ // Project languages
200
+ projectLanguages,
201
+ pending,
202
+ refresh,
203
+ adding,
204
+ addLanguage,
205
+ deleting,
206
+ deleteLanguage,
207
+ setDefault,
208
+ setFallback,
209
+ startTranslateAll,
210
+ // Translation job
211
+ showProgress,
212
+ progressLangName,
213
+ progressTotal,
214
+ progressDone,
215
+ progressPercent,
216
+ progressStatus,
217
+ startPolling,
218
+ closeProgress,
219
+ sendToBackground,
220
+ }
221
+ }
@@ -0,0 +1,76 @@
1
+ import { profileService } from '../services/profile.service'
2
+ import { authService } from '../services/auth.service'
3
+ import { userService } from '../services/user.service'
4
+
5
+ export function useProfile(userId?: MaybeRefOrGetter<number | string>) {
6
+ const toast = useToast()
7
+ const { fetchMe } = useAuth()
8
+
9
+ // ── Profile data ─────────────────────────────────────────────────────────
10
+ // Load the target user's profile (or own profile if no userId provided)
11
+
12
+ const targetId = computed(() => userId ? toValue(userId) : null)
13
+
14
+ const { data: profile, pending, refresh } = useAsyncData(
15
+ () => targetId.value ? `user-profile-${targetId.value}` : 'user-profile',
16
+ () => targetId.value
17
+ ? profileService.getUserProfile(targetId.value)
18
+ : profileService.getProfile(),
19
+ { watch: [targetId] },
20
+ )
21
+
22
+ // ── Own account editing (current logged-in user) ─────────────────────────
23
+
24
+ const editSaving = ref(false)
25
+ const editError = ref('')
26
+
27
+ async function updateProfile(name: string, email: string): Promise<boolean> {
28
+ editError.value = ''
29
+ editSaving.value = true
30
+ try {
31
+ await authService.updateMe({ name, email })
32
+ await Promise.all([refresh(), fetchMe()])
33
+ toast.add({ title: 'Compte mis à jour', color: 'success' })
34
+ return true
35
+ }
36
+ catch (e: any) {
37
+ editError.value = e.message
38
+ return false
39
+ }
40
+ finally {
41
+ editSaving.value = false
42
+ }
43
+ }
44
+
45
+ // ── Role management (admin managing another user's access) ───────────────
46
+
47
+ const rolesSaving = ref(false)
48
+
49
+ async function saveRoles(roles: Array<{ project_id: number | null; role: string | null }>): Promise<boolean> {
50
+ if (!targetId.value) return false
51
+ rolesSaving.value = true
52
+ try {
53
+ await userService.updateRoles(Number(targetId.value), roles)
54
+ toast.add({ title: 'Accès mis à jour', color: 'success' })
55
+ await refresh()
56
+ return true
57
+ }
58
+ catch {
59
+ return false
60
+ }
61
+ finally {
62
+ rolesSaving.value = false
63
+ }
64
+ }
65
+
66
+ return {
67
+ profile,
68
+ pending,
69
+ refresh,
70
+ editSaving,
71
+ editError,
72
+ updateProfile,
73
+ rolesSaving,
74
+ saveRoles,
75
+ }
76
+ }
@@ -0,0 +1,180 @@
1
+ import { projectService } from '~/services/project.service'
2
+ import { scanService } from '~/services/scan.service'
3
+ import type { ProjectPayload } from '~/interfaces/project.interface'
4
+
5
+ export interface Project {
6
+ id: number
7
+ name: string
8
+ root_path: string
9
+ source_url?: string
10
+ locales_path: string
11
+ key_separator: string
12
+ color: string
13
+ description?: string
14
+ key_count?: number
15
+ language_count?: number
16
+ is_system?: boolean
17
+ }
18
+
19
+ export function canScanProject(project: Project): boolean {
20
+ return !!project.root_path && project.root_path !== '__DASHBOARD_UI__'
21
+ }
22
+
23
+ export function canSyncProject(project: Project): boolean {
24
+ return (!!project.root_path && project.root_path !== '__DASHBOARD_UI__') || !!project.source_url
25
+ }
26
+
27
+ /**
28
+ * Shared reactive project list — NOT route-dependent.
29
+ * Composables (useStats, useProjectLanguages, etc.) that need the project list
30
+ * should call useProject(); only pages that need the current project from URL
31
+ * should read `currentProject`.
32
+ */
33
+ export function useProject() {
34
+ const { currentUser } = useAuth()
35
+
36
+ const { data: projectsData, pending, refresh: fetchProjects } = useAsyncData<Project[]>(
37
+ 'all-projects',
38
+ () => projectService.getAll(),
39
+ { default: () => [] },
40
+ )
41
+
42
+ const projects = computed(() => (projectsData.value ?? []) as Project[])
43
+
44
+ // Not route-dependent — safe to watch from anywhere (including layout onMount)
45
+ const systemProject = computed(() => projects.value.find(p => p.is_system) ?? null)
46
+
47
+ // Route-dependent: only returns a value when inside a /projects/[id]/* page
48
+ const route = useRoute()
49
+ const currentProject = computed((): Project | null => {
50
+ const paramId = route.params.id
51
+ if (!paramId) return null
52
+ const id = Number(Array.isArray(paramId) ? paramId[0] : paramId)
53
+ return projects.value.find(p => p.id === id) ?? null
54
+ })
55
+
56
+ const visibleProjects = computed(() => {
57
+ if (currentUser.value?.is_super_admin) return projects.value
58
+ const userProjectIds = new Set(
59
+ (currentUser.value?.roles ?? [])
60
+ .filter((r: any) => r.project_id !== null)
61
+ .map((r: any) => r.project_id),
62
+ )
63
+ return projects.value.filter((p: any) => userProjectIds.has(p.id))
64
+ })
65
+
66
+ // ── Mutations ───────────────────────────────────────────────────────────────
67
+
68
+ const toast = useToast()
69
+ const router = useRouter()
70
+
71
+ const saving = ref(false)
72
+ async function createProject(payload: ProjectPayload): Promise<any> {
73
+ saving.value = true
74
+ try {
75
+ const project = await projectService.create(payload)
76
+ toast.add({ title: 'Projet ajouté', color: 'success' })
77
+ await fetchProjects()
78
+ return project
79
+ }
80
+ catch {
81
+ return null
82
+ }
83
+ finally {
84
+ saving.value = false
85
+ }
86
+ }
87
+
88
+ async function updateProject(id: number, payload: Partial<ProjectPayload>): Promise<boolean> {
89
+ saving.value = true
90
+ try {
91
+ await projectService.update(id, payload)
92
+ toast.add({ title: 'Projet modifié', color: 'success' })
93
+ await fetchProjects()
94
+ return true
95
+ }
96
+ catch {
97
+ return false
98
+ }
99
+ finally {
100
+ saving.value = false
101
+ }
102
+ }
103
+
104
+ const deleting = ref(false)
105
+ async function deleteProject(id: number): Promise<boolean> {
106
+ deleting.value = true
107
+ try {
108
+ await projectService.remove(id)
109
+ toast.add({ title: 'Projet supprimé', color: 'success' })
110
+ await fetchProjects()
111
+ router.push('/projects')
112
+ return true
113
+ }
114
+ catch {
115
+ return false
116
+ }
117
+ finally {
118
+ deleting.value = false
119
+ }
120
+ }
121
+
122
+ // ── Scan / Sync ─────────────────────────────────────────────────────────────
123
+
124
+ const scanning = ref<number | null>(null)
125
+ async function scanProject(project: { id: number; name: string }): Promise<void> {
126
+ scanning.value = project.id
127
+ try {
128
+ const result = await scanService.scan(project.id)
129
+ const langMsg = result.langsAdded > 0 ? ` · ${result.langsAdded} langue(s) ajoutée(s)` : ''
130
+ toast.add({
131
+ title: `Scan — ${project.name}`,
132
+ description: `${result.keysFound} clés dans ${result.scannedFiles} fichiers · ${result.keysAdded} nouvelles${langMsg}`,
133
+ color: 'success',
134
+ })
135
+ await fetchProjects()
136
+ refreshNuxtData('project-stats')
137
+ }
138
+ catch {}
139
+ finally {
140
+ scanning.value = null
141
+ }
142
+ }
143
+
144
+ const syncing = ref<number | null>(null)
145
+ async function syncProject(project: { id: number; name: string }): Promise<void> {
146
+ syncing.value = project.id
147
+ try {
148
+ const result = await scanService.sync(project.id)
149
+ toast.add({
150
+ title: `Sync — ${project.name}`,
151
+ description: `${result.added} ajoutées · ${result.updated} mises à jour · ${result.total} total`,
152
+ color: 'success',
153
+ })
154
+ await fetchProjects()
155
+ refreshNuxtData('project-stats')
156
+ }
157
+ catch {}
158
+ finally {
159
+ syncing.value = null
160
+ }
161
+ }
162
+
163
+ return {
164
+ currentProject,
165
+ projects,
166
+ visibleProjects,
167
+ systemProject,
168
+ fetchProjects,
169
+ pending,
170
+ saving,
171
+ createProject,
172
+ updateProject,
173
+ deleting,
174
+ deleteProject,
175
+ scanning,
176
+ scanProject,
177
+ syncing,
178
+ syncProject,
179
+ }
180
+ }