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.
- package/LICENSE +21 -0
- package/README.md +715 -0
- package/app.vue +8 -0
- package/assets/css/main.css +21 -0
- package/assets/locales/en.json +380 -0
- package/bin/cli.mjs +279 -0
- package/components/LinkedKeyPicker.vue +135 -0
- package/components/PathPicker.vue +153 -0
- package/components/PluralEditor.vue +295 -0
- package/components/ScanModal.vue +153 -0
- package/components/TranslationHistoryModal.vue +66 -0
- package/components/TranslationRow.vue +541 -0
- package/components/dashboard/WidgetConfigModal.vue +121 -0
- package/components/dashboard/WidgetGrid.vue +190 -0
- package/components/dashboard/WidgetPicker.vue +75 -0
- package/components/dashboard/widgets/ActivityWidget.vue +109 -0
- package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
- package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
- package/components/dashboard/widgets/ReviewWidget.vue +150 -0
- package/components/dashboard/widgets/StatWidget.vue +133 -0
- package/composables/useAuth.ts +72 -0
- package/composables/useConfig.ts +14 -0
- package/composables/useDashboard.ts +89 -0
- package/composables/useFormats.ts +100 -0
- package/composables/useKeys.ts +231 -0
- package/composables/useLanguages.ts +221 -0
- package/composables/useProfile.ts +76 -0
- package/composables/useProject.ts +180 -0
- package/composables/useReview.ts +94 -0
- package/composables/useSettings.ts +30 -0
- package/composables/useStats.ts +16 -0
- package/composables/useT.ts +38 -0
- package/composables/useUsers.ts +101 -0
- package/composables/useWidgetData.ts +50 -0
- package/consts/commons.const.ts +6 -0
- package/consts/dashboard.const.ts +94 -0
- package/consts/languages.const.ts +223 -0
- package/enums/commons.enum.ts +7 -0
- package/i18n-dashboard.config.example.js +40 -0
- package/interfaces/commons.interface.ts +23 -0
- package/interfaces/job.interface.ts +10 -0
- package/interfaces/key.interface.ts +39 -0
- package/interfaces/languages.interface.ts +23 -0
- package/interfaces/project.interface.ts +9 -0
- package/interfaces/scan.interface.ts +12 -0
- package/interfaces/settings.interface.ts +4 -0
- package/interfaces/stat.interface.ts +30 -0
- package/interfaces/translation.interface.ts +11 -0
- package/interfaces/user.interface.ts +24 -0
- package/layouts/auth.vue +5 -0
- package/layouts/default.vue +327 -0
- package/middleware/auth.global.ts +26 -0
- package/nuxt.config.ts +66 -0
- package/package.json +89 -0
- package/pages/index.vue +5 -0
- package/pages/login.vue +74 -0
- package/pages/onboarding.vue +563 -0
- package/pages/projects/[id]/formats/datetime.vue +240 -0
- package/pages/projects/[id]/formats/modifiers.vue +194 -0
- package/pages/projects/[id]/formats/number.vue +250 -0
- package/pages/projects/[id]/index.vue +182 -0
- package/pages/projects/[id]/languages.vue +537 -0
- package/pages/projects/[id]/review.vue +109 -0
- package/pages/projects/[id]/settings.vue +515 -0
- package/pages/projects/[id]/translations/[keyId].vue +642 -0
- package/pages/projects/[id]/translations/index.vue +250 -0
- package/pages/projects/[id]/users.vue +276 -0
- package/pages/projects/index.vue +334 -0
- package/pages/users/[id]/profile.vue +421 -0
- package/pages/users/index.vue +345 -0
- package/plugins/loading.client.ts +3 -0
- package/plugins/ui-i18n.ts +6 -0
- package/server/api/auth/login.post.ts +28 -0
- package/server/api/auth/logout.post.ts +7 -0
- package/server/api/auth/me.get.ts +11 -0
- package/server/api/auth/me.put.ts +31 -0
- package/server/api/auth/password.put.ts +27 -0
- package/server/api/auth/status.get.ts +16 -0
- package/server/api/config.get.ts +10 -0
- package/server/api/dashboard/layout.get.ts +18 -0
- package/server/api/dashboard/layout.post.ts +18 -0
- package/server/api/db-config.get.ts +44 -0
- package/server/api/db-config.post.ts +73 -0
- package/server/api/export.get.ts +64 -0
- package/server/api/formats/datetime/[id].delete.ts +8 -0
- package/server/api/formats/datetime/[id].put.ts +15 -0
- package/server/api/formats/datetime.get.ts +11 -0
- package/server/api/formats/datetime.post.ts +16 -0
- package/server/api/formats/modifiers/[id].delete.ts +8 -0
- package/server/api/formats/modifiers/[id].put.ts +10 -0
- package/server/api/formats/modifiers.get.ts +10 -0
- package/server/api/formats/modifiers.post.ts +14 -0
- package/server/api/formats/number/[id].delete.ts +8 -0
- package/server/api/formats/number/[id].put.ts +15 -0
- package/server/api/formats/number.get.ts +11 -0
- package/server/api/formats/number.post.ts +16 -0
- package/server/api/formats/snippet.get.ts +87 -0
- package/server/api/fs/browse.get.ts +50 -0
- package/server/api/history/[translationId].get.ts +13 -0
- package/server/api/keys/[id].delete.ts +14 -0
- package/server/api/keys/[id].get.ts +41 -0
- package/server/api/keys/[id].patch.ts +20 -0
- package/server/api/keys/index.get.ts +98 -0
- package/server/api/keys/index.post.ts +17 -0
- package/server/api/languages/[code].delete.ts +15 -0
- package/server/api/languages/[id].put.ts +24 -0
- package/server/api/languages/index.get.ts +13 -0
- package/server/api/languages/index.post.ts +42 -0
- package/server/api/onboarding.post.ts +56 -0
- package/server/api/profile.get.ts +81 -0
- package/server/api/project-snapshot.get.ts +73 -0
- package/server/api/project-snapshot.post.ts +160 -0
- package/server/api/projects/[id].delete.ts +13 -0
- package/server/api/projects/[id].put.ts +40 -0
- package/server/api/projects/index.get.ts +19 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/scan.post.ts +165 -0
- package/server/api/settings/index.get.ts +9 -0
- package/server/api/settings/index.post.ts +20 -0
- package/server/api/setup.post.ts +39 -0
- package/server/api/stats/global.get.ts +126 -0
- package/server/api/stats.get.ts +70 -0
- package/server/api/sync.post.ts +179 -0
- package/server/api/translate.post.ts +52 -0
- package/server/api/translations/batch-translate.post.ts +121 -0
- package/server/api/translations/bulk-status.post.ts +24 -0
- package/server/api/translations/index.post.ts +62 -0
- package/server/api/translations/job/[id].get.ts +23 -0
- package/server/api/translations/status.post.ts +30 -0
- package/server/api/translations/translate-all.post.ts +18 -0
- package/server/api/ui-locale.get.ts +39 -0
- package/server/api/users/[id]/profile.get.ts +107 -0
- package/server/api/users/[id]/roles.put.ts +67 -0
- package/server/api/users/[id].delete.ts +36 -0
- package/server/api/users/[id].put.ts +43 -0
- package/server/api/users/index.get.ts +49 -0
- package/server/api/users/index.post.ts +89 -0
- package/server/consts/auto-translate.const.ts +2 -0
- package/server/consts/commons.const.ts +10 -0
- package/server/consts/db.const.ts +3 -0
- package/server/consts/scanner.const.ts +4 -0
- package/server/consts/translation-job.const.ts +8 -0
- package/server/db/index.ts +672 -0
- package/server/enums/auth.enum.ts +5 -0
- package/server/enums/translation.enum.ts +6 -0
- package/server/interfaces/profile.interface.ts +48 -0
- package/server/interfaces/project-config.interface.ts +9 -0
- package/server/interfaces/scanner.interface.ts +18 -0
- package/server/interfaces/translation-job.interface.ts +13 -0
- package/server/middleware/auth.ts +32 -0
- package/server/plugins/db.ts +6 -0
- package/server/routes/locale/[lang].get.ts +179 -0
- package/server/types/auth.type.ts +3 -0
- package/server/utils/auth.util.ts +89 -0
- package/server/utils/auto-translate.util.ts +112 -0
- package/server/utils/lang-api.util.ts +24 -0
- package/server/utils/mailer.util.ts +80 -0
- package/server/utils/project-config.util.ts +37 -0
- package/server/utils/scanner.uti.ts +307 -0
- package/server/utils/translation-job.util.ts +142 -0
- package/services/auth.service.ts +31 -0
- package/services/base.service.ts +140 -0
- package/services/job.service.ts +10 -0
- package/services/key.service.ts +26 -0
- package/services/language.service.ts +26 -0
- package/services/profile.service.ts +14 -0
- package/services/project.service.ts +23 -0
- package/services/scan.service.ts +14 -0
- package/services/settings.service.ts +14 -0
- package/services/stats.service.ts +11 -0
- package/services/translation.service.ts +36 -0
- package/services/user.service.ts +28 -0
- package/tsconfig.json +3 -0
- package/types/commons.type.ts +3 -0
- package/types/dashboard.type.ts +26 -0
- 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
|
+
}
|