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,94 @@
1
+ import { keyService } from '../services/key.service'
2
+ import { translationService } from '../services/translation.service'
3
+ import { TRANSLATION_STATUS } from '../server/enums/translation.enum'
4
+
5
+ export interface ReviewItem {
6
+ id: number
7
+ key: string
8
+ key_description?: string
9
+ language_code: string
10
+ value: string
11
+ }
12
+
13
+ export function useReview() {
14
+ const toast = useToast()
15
+ const { currentProject } = useProject()
16
+
17
+ const data = ref<any>(null)
18
+ const pending = ref(false)
19
+
20
+ async function refresh() {
21
+ if (!currentProject.value?.id) return
22
+ pending.value = true
23
+ try {
24
+ data.value = await keyService.getKeys({
25
+ project_id: currentProject.value.id,
26
+ status: TRANSLATION_STATUS.DRAFT,
27
+ limit: 200,
28
+ })
29
+ } catch {} finally {
30
+ pending.value = false
31
+ }
32
+ }
33
+
34
+ watch(() => currentProject.value?.id, refresh, { immediate: true })
35
+
36
+ const reviewItems = computed<ReviewItem[]>(() => {
37
+ const keys = data.value?.data ?? []
38
+ const result: ReviewItem[] = []
39
+ for (const k of keys) {
40
+ for (const [lang, tr] of Object.entries(k.translations as Record<string, any>)) {
41
+ if (tr?.status === TRANSLATION_STATUS.DRAFT && tr?.value) {
42
+ result.push({ id: tr.id, key: k.key, key_description: k.description, language_code: lang, value: tr.value })
43
+ }
44
+ }
45
+ }
46
+ return result
47
+ })
48
+
49
+ const processingId = ref<number | null>(null)
50
+ const processingAction = ref('')
51
+
52
+ async function setStatus(item: ReviewItem, status: TRANSLATION_STATUS): Promise<void> {
53
+ processingId.value = item.id
54
+ processingAction.value = status
55
+ try {
56
+ await translationService.bulkStatus([item.id], status)
57
+ await refresh()
58
+ refreshNuxtData('project-stats')
59
+ } catch {} finally {
60
+ processingId.value = null
61
+ processingAction.value = ''
62
+ }
63
+ }
64
+
65
+ const approvingAll = ref(false)
66
+ async function markAllReviewed(): Promise<void> {
67
+ approvingAll.value = true
68
+ try {
69
+ const ids = reviewItems.value.map(i => i.id)
70
+ await translationService.bulkStatus(ids, TRANSLATION_STATUS.REVIEWED)
71
+ const n = ids.length
72
+ toast.add({
73
+ title: 'Relues',
74
+ description: `${n} traduction${n > 1 ? 's' : ''} marquée${n > 1 ? 's' : ''} comme relue${n > 1 ? 's' : ''}`,
75
+ color: 'success',
76
+ })
77
+ await refresh()
78
+ refreshNuxtData('project-stats')
79
+ } catch {} finally {
80
+ approvingAll.value = false
81
+ }
82
+ }
83
+
84
+ return {
85
+ reviewItems,
86
+ pending,
87
+ refresh,
88
+ processingId,
89
+ processingAction,
90
+ setStatus,
91
+ approvingAll,
92
+ markAllReviewed,
93
+ }
94
+ }
@@ -0,0 +1,30 @@
1
+ import { settingsService } from '../services/settings.service'
2
+ import type { SettingsPayload } from '../interfaces/settings.interface'
3
+
4
+ export function useSettings() {
5
+ const toast = useToast()
6
+
7
+ const { data, pending, refresh } = useAsyncData(
8
+ 'settings',
9
+ () => settingsService.getSettings(),
10
+ { default: () => ({} as Record<string, string>) },
11
+ )
12
+
13
+ const settings = computed(() => data.value ?? {})
14
+
15
+ const saving = ref(false)
16
+ async function saveSettings(payload: SettingsPayload): Promise<void> {
17
+ saving.value = true
18
+ try {
19
+ await settingsService.saveSettings(payload)
20
+ toast.add({ title: 'Paramètres enregistrés', color: 'success' })
21
+ await refresh()
22
+ } catch (e: any) {
23
+ toast.add({ title: 'Erreur', description: e.message, color: 'error' })
24
+ } finally {
25
+ saving.value = false
26
+ }
27
+ }
28
+
29
+ return { settings, pending, refresh, saving, saveSettings }
30
+ }
@@ -0,0 +1,16 @@
1
+ import { statsService } from '../services/stats.service'
2
+ import type { StatsResponse } from '../services/stats.service'
3
+
4
+ export function useStats() {
5
+ const { currentProject } = useProject()
6
+
7
+ const { data, pending, refresh } = useAsyncData<StatsResponse | null>(
8
+ 'project-stats',
9
+ () => statsService.getStats(currentProject.value?.id),
10
+ { watch: [() => currentProject.value?.id] },
11
+ )
12
+
13
+ const stats = computed(() => data.value ?? null)
14
+
15
+ return { stats, pending, refresh }
16
+ }
@@ -0,0 +1,38 @@
1
+ // Dashboard UI i18n composable
2
+ // Uses the "Dashboard UI" system project translations for the interface language.
3
+
4
+ import { languageService } from '../services/language.service'
5
+
6
+ const _uiTranslations = ref<Record<string, string>>({})
7
+ const _uiLang = ref('en')
8
+
9
+ export function useT() {
10
+ const translations = _uiTranslations
11
+ const lang = _uiLang
12
+
13
+ const loadTranslations = async (language: string)=> {
14
+ try {
15
+ const data = await $fetch<Record<string, string>>(`/api/ui-locale?lang=${language}`)
16
+ translations.value = data
17
+ lang.value = language
18
+ } catch {
19
+ // If the endpoint fails, keep whatever we have
20
+ }
21
+ }
22
+
23
+ const t = (key: string, fallback?: string): string => {
24
+ return translations.value[key] ?? fallback ?? key
25
+ }
26
+
27
+ const setLang = async (language: string) => {
28
+ await loadTranslations(language)
29
+ const cookie = useCookie('ui-lang', { maxAge: 60 * 60 * 24 * 365 })
30
+ cookie.value = language
31
+ }
32
+
33
+ const getLangs = async (currentProjectId: string | number) => {
34
+ return languageService.getLanguages(Number(currentProjectId))
35
+ }
36
+
37
+ return { t, lang, loadTranslations, setLang, getLangs }
38
+ }
@@ -0,0 +1,101 @@
1
+ import { userService } from '../services/user.service'
2
+ import type { CreateUserPayload, RoleEntry } from '../interfaces/user.interface'
3
+
4
+ export function useUsers(scope: 'project' | 'global' = 'project') {
5
+ const toast = useToast()
6
+ const { currentUser } = useAuth()
7
+ const { currentProject } = useProject()
8
+
9
+ const usersQuery = computed(() =>
10
+ scope === 'global' ? {} : { project_id: currentProject.value?.id },
11
+ )
12
+
13
+ const { data, pending, refresh } = useAsyncData(
14
+ scope === 'global' ? 'users-global' : 'users',
15
+ () => userService.getUsers(usersQuery.value),
16
+ { watch: [usersQuery], default: () => [] },
17
+ )
18
+
19
+ const users = computed(() => data.value ?? [])
20
+
21
+ // ── Mutations ──────────────────────────────────────────────────────────────
22
+
23
+ const saving = ref(false)
24
+ async function createUser(payload: CreateUserPayload): Promise<string | null> {
25
+ saving.value = true
26
+ try {
27
+ const result = await userService.create(payload)
28
+ toast.add({
29
+ title: 'Utilisateur créé',
30
+ description: `Email d'invitation envoyé à ${payload.email}`,
31
+ color: 'success',
32
+ })
33
+ await refresh()
34
+ return result.tempPassword
35
+ } catch {
36
+ return null
37
+ } finally {
38
+ saving.value = false
39
+ }
40
+ }
41
+
42
+ const rolesSaving = ref(false)
43
+ async function updateRoles(userId: number, roles: RoleEntry[]): Promise<boolean> {
44
+ rolesSaving.value = true
45
+ try {
46
+ await userService.updateRoles(userId, roles)
47
+ toast.add({ title: 'Accès mis à jour', color: 'success' })
48
+ await refresh()
49
+ return true
50
+ } catch {
51
+ return false
52
+ } finally {
53
+ rolesSaving.value = false
54
+ }
55
+ }
56
+
57
+ async function toggleActive(user: { id: number; is_active: boolean }): Promise<void> {
58
+ try {
59
+ await userService.update(user.id, {
60
+ is_active: !user.is_active,
61
+ project_id: scope === 'project' ? currentProject.value?.id : undefined,
62
+ })
63
+ toast.add({
64
+ title: user.is_active ? 'Utilisateur désactivé' : 'Utilisateur réactivé',
65
+ color: 'success',
66
+ })
67
+ await refresh()
68
+ } catch {}
69
+ }
70
+
71
+ const deleting = ref(false)
72
+ async function deleteUser(userId: number): Promise<boolean> {
73
+ deleting.value = true
74
+ try {
75
+ const projectId = scope === 'global' || currentUser.value?.is_super_admin
76
+ ? undefined
77
+ : currentProject.value?.id
78
+ await userService.remove(userId, projectId)
79
+ toast.add({ title: 'Utilisateur supprimé', color: 'success' })
80
+ await refresh()
81
+ return true
82
+ } catch {
83
+ return false
84
+ } finally {
85
+ deleting.value = false
86
+ }
87
+ }
88
+
89
+ return {
90
+ users,
91
+ pending,
92
+ refresh,
93
+ saving,
94
+ createUser,
95
+ rolesSaving,
96
+ updateRoles,
97
+ toggleActive,
98
+ deleting,
99
+ deleteUser,
100
+ }
101
+ }
@@ -0,0 +1,50 @@
1
+ import type { Ref } from 'vue'
2
+ import type { WidgetDataSource } from '~/types/dashboard.type'
3
+
4
+ export function useWidgetData(widgetId: string, dataSource: Ref<WidgetDataSource | undefined>) {
5
+ const { currentProject, projects } = useProject()
6
+
7
+ const effectiveSource = computed(() => dataSource.value ?? { type: 'global' as const })
8
+
9
+ const fetchKey = computed(() => {
10
+ const src = effectiveSource.value
11
+ if (src.type === 'project') return `widget-stats-${widgetId}-project-${src.projectId}`
12
+ return `widget-stats-${widgetId}-global`
13
+ })
14
+
15
+ const projectId = computed(() => {
16
+ const src = effectiveSource.value
17
+ if (src.type === 'project') return src.projectId
18
+ return undefined
19
+ })
20
+
21
+ const { data: stats, pending, refresh } = useAsyncData(
22
+ () => fetchKey.value,
23
+ async () => {
24
+ const src = effectiveSource.value
25
+ if (src.type === 'project') {
26
+ if (!src.projectId) return null
27
+ return await $fetch<any>('/api/stats', { query: { project_id: src.projectId } })
28
+ }
29
+ return await $fetch<any>('/api/stats/global')
30
+ },
31
+ { server: false, watch: [fetchKey] },
32
+ )
33
+
34
+ const sourceLabel = computed(() => {
35
+ const src = effectiveSource.value
36
+ if (src.type === 'project') {
37
+ const p = projects.value.find((p: any) => p.id === src.projectId)
38
+ return p?.name ?? 'Projet'
39
+ }
40
+ return 'Global'
41
+ })
42
+
43
+ const hasProject = computed(() => {
44
+ const src = effectiveSource.value
45
+ if (src.type === 'global') return true
46
+ return !!src.projectId
47
+ })
48
+
49
+ return { stats, pending, refresh, sourceLabel, hasProject, effectiveSource }
50
+ }
@@ -0,0 +1,6 @@
1
+ import { dirname } from 'path'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ export const _dirname = typeof __dirname !== 'undefined'
5
+ ? __dirname
6
+ : dirname(fileURLToPath(import.meta.url))
@@ -0,0 +1,94 @@
1
+ import type { WidgetSize, WidgetType, WidgetConfig } from '../types/dashboard.type'
2
+
3
+ export const WIDGET_SIZES: Record<WidgetSize, { cols: number; rows: number; label: string }> = {
4
+ sm: { cols: 1, rows: 1, label: 'Petit' },
5
+ md: { cols: 2, rows: 1, label: 'Moyen' },
6
+ lg: { cols: 2, rows: 2, label: 'Grand' },
7
+ wide: { cols: 4, rows: 1, label: 'Large' },
8
+ xl: { cols: 4, rows: 2, label: 'Très grand' },
9
+ }
10
+
11
+ export const WIDGET_SIZE_CLASSES: Record<WidgetSize, string> = {
12
+ sm: 'col-span-1 row-span-1',
13
+ md: 'col-span-2 row-span-1',
14
+ lg: 'col-span-2 row-span-2',
15
+ wide: 'col-span-4 row-span-1',
16
+ xl: 'col-span-4 row-span-2',
17
+ }
18
+
19
+ export const WIDGET_REGISTRY: Record<WidgetType, { label: string; description: string; icon: string; sizes: WidgetSize[]; defaultSize: WidgetSize; hasDataSource: boolean }> = {
20
+ 'stat-keys': {
21
+ label: 'Clés totales',
22
+ description: 'Nombre total de clés de traduction',
23
+ icon: 'i-heroicons-key',
24
+ sizes: ['sm', 'md'],
25
+ defaultSize: 'sm',
26
+ hasDataSource: true,
27
+ },
28
+ 'stat-coverage': {
29
+ label: 'Couverture',
30
+ description: 'Taux de couverture global des traductions',
31
+ icon: 'i-heroicons-chart-bar',
32
+ sizes: ['sm', 'md'],
33
+ defaultSize: 'sm',
34
+ hasDataSource: true,
35
+ },
36
+ 'stat-languages': {
37
+ label: 'Langues',
38
+ description: 'Nombre de langues configurées',
39
+ icon: 'i-heroicons-language',
40
+ sizes: ['sm', 'md'],
41
+ defaultSize: 'sm',
42
+ hasDataSource: true,
43
+ },
44
+ 'stat-unused': {
45
+ label: 'Clés inutilisées',
46
+ description: 'Clés non trouvées dans le code source',
47
+ icon: 'i-heroicons-exclamation-triangle',
48
+ sizes: ['sm', 'md'],
49
+ defaultSize: 'sm',
50
+ hasDataSource: true,
51
+ },
52
+ 'projects': {
53
+ label: 'Projets',
54
+ description: 'Liste de vos projets de traduction',
55
+ icon: 'i-heroicons-rectangle-stack',
56
+ sizes: ['md', 'lg', 'wide'],
57
+ defaultSize: 'wide',
58
+ hasDataSource: false,
59
+ },
60
+ 'languages-coverage': {
61
+ label: 'Couverture par langue',
62
+ description: 'Progression de chaque langue',
63
+ icon: 'i-heroicons-globe-alt',
64
+ sizes: ['md', 'lg', 'wide'],
65
+ defaultSize: 'md',
66
+ hasDataSource: true,
67
+ },
68
+ 'last-activity': {
69
+ label: 'Activité récente',
70
+ description: 'Dernières modifications de traductions',
71
+ icon: 'i-heroicons-clock',
72
+ sizes: ['md', 'lg', 'wide'],
73
+ defaultSize: 'md',
74
+ hasDataSource: true,
75
+ },
76
+ 'review-queue': {
77
+ label: 'File de révision',
78
+ description: 'Traductions en attente de validation',
79
+ icon: 'i-heroicons-clipboard-document-check',
80
+ sizes: ['md', 'lg', 'wide'],
81
+ defaultSize: 'md',
82
+ hasDataSource: true,
83
+ },
84
+ }
85
+
86
+ export const DEFAULT_LAYOUT: WidgetConfig[] = [
87
+ { id: 'default-1', type: 'stat-keys', size: 'sm' },
88
+ { id: 'default-2', type: 'stat-coverage', size: 'sm' },
89
+ { id: 'default-3', type: 'stat-languages', size: 'sm' },
90
+ { id: 'default-4', type: 'stat-unused', size: 'sm' },
91
+ { id: 'default-5', type: 'projects', size: 'wide' },
92
+ { id: 'default-6', type: 'languages-coverage', size: 'md' },
93
+ { id: 'default-7', type: 'last-activity', size: 'md' },
94
+ ]
@@ -0,0 +1,223 @@
1
+ import type { Language } from '../interfaces/languages.interface'
2
+
3
+ export const LANGUAGES: Language[] = [
4
+ // ── A ──────────────────────────────────────────────────────────────────────
5
+ { code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' },
6
+ { code: 'sq', name: 'Albanian', nativeName: 'Shqip' },
7
+ { code: 'am', name: 'Amharic', nativeName: 'አማርኛ' },
8
+ { code: 'ar', name: 'Arabic', nativeName: 'العربية' },
9
+ { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', nativeName: 'العربية (السعودية)' },
10
+ { code: 'ar-EG', name: 'Arabic (Egypt)', nativeName: 'العربية (مصر)' },
11
+ { code: 'ar-MA', name: 'Arabic (Morocco)', nativeName: 'العربية (المغرب)' },
12
+ { code: 'ar-DZ', name: 'Arabic (Algeria)', nativeName: 'العربية (الجزائر)' },
13
+ { code: 'ar-IQ', name: 'Arabic (Iraq)', nativeName: 'العربية (العراق)' },
14
+ { code: 'ar-SY', name: 'Arabic (Syria)', nativeName: 'العربية (سوريا)' },
15
+ { code: 'hy', name: 'Armenian', nativeName: 'Հայերեն' },
16
+ { code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' },
17
+
18
+ // ── B ──────────────────────────────────────────────────────────────────────
19
+ { code: 'eu', name: 'Basque', nativeName: 'Euskara' },
20
+ { code: 'be', name: 'Belarusian', nativeName: 'Беларуская' },
21
+ { code: 'bn', name: 'Bengali', nativeName: 'বাংলা' },
22
+ { code: 'bn-BD', name: 'Bengali (Bangladesh)', nativeName: 'বাংলা (বাংলাদেশ)' },
23
+ { code: 'bn-IN', name: 'Bengali (India)', nativeName: 'বাংলা (ভারত)' },
24
+ { code: 'bs', name: 'Bosnian', nativeName: 'Bosanski' },
25
+ { code: 'bg', name: 'Bulgarian', nativeName: 'Български' },
26
+
27
+ // ── C ──────────────────────────────────────────────────────────────────────
28
+ { code: 'ca', name: 'Catalan', nativeName: 'Català' },
29
+ { code: 'ceb', name: 'Cebuano', nativeName: 'Cebuano' },
30
+ { code: 'zh', name: 'Chinese (Simplified)', nativeName: '中文(简体)' },
31
+ { code: 'zh-CN', name: 'Chinese (Simplified, China)', nativeName: '中文(简体, 中国)' },
32
+ { code: 'zh-SG', name: 'Chinese (Simplified, Singapore)', nativeName: '中文(简体, 新加坡)' },
33
+ { code: 'zh-TW', name: 'Chinese (Traditional, Taiwan)', nativeName: '中文(繁體, 台灣)' },
34
+ { code: 'zh-HK', name: 'Chinese (Traditional, Hong Kong)', nativeName: '中文(繁體, 香港)' },
35
+ { code: 'zh-MO', name: 'Chinese (Traditional, Macao)', nativeName: '中文(繁體, 澳門)' },
36
+ { code: 'co', name: 'Corsican', nativeName: 'Corsu' },
37
+ { code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
38
+ { code: 'cs', name: 'Czech', nativeName: 'Čeština' },
39
+
40
+ // ── D ──────────────────────────────────────────────────────────────────────
41
+ { code: 'da', name: 'Danish', nativeName: 'Dansk' },
42
+ { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
43
+ { code: 'nl-NL', name: 'Dutch (Netherlands)', nativeName: 'Nederlands (Nederland)' },
44
+ { code: 'nl-BE', name: 'Dutch (Belgium)', nativeName: 'Nederlands (België)' },
45
+
46
+ // ── E ──────────────────────────────────────────────────────────────────────
47
+ { code: 'en', name: 'English', nativeName: 'English' },
48
+ { code: 'en-US', name: 'English (United States)', nativeName: 'English (US)' },
49
+ { code: 'en-GB', name: 'English (United Kingdom)', nativeName: 'English (UK)' },
50
+ { code: 'en-CA', name: 'English (Canada)', nativeName: 'English (Canada)' },
51
+ { code: 'en-AU', name: 'English (Australia)', nativeName: 'English (Australia)' },
52
+ { code: 'en-NZ', name: 'English (New Zealand)', nativeName: 'English (New Zealand)' },
53
+ { code: 'en-IE', name: 'English (Ireland)', nativeName: 'English (Ireland)' },
54
+ { code: 'en-IN', name: 'English (India)', nativeName: 'English (India)' },
55
+ { code: 'en-ZA', name: 'English (South Africa)', nativeName: 'English (South Africa)' },
56
+ { code: 'en-SG', name: 'English (Singapore)', nativeName: 'English (Singapore)' },
57
+ { code: 'en-NG', name: 'English (Nigeria)', nativeName: 'English (Nigeria)' },
58
+ { code: 'en-PH', name: 'English (Philippines)', nativeName: 'English (Philippines)' },
59
+ { code: 'eo', name: 'Esperanto', nativeName: 'Esperanto' },
60
+ { code: 'et', name: 'Estonian', nativeName: 'Eesti' },
61
+
62
+ // ── F ──────────────────────────────────────────────────────────────────────
63
+ { code: 'fi', name: 'Finnish', nativeName: 'Suomi' },
64
+ { code: 'fr', name: 'French', nativeName: 'Français' },
65
+ { code: 'fr-FR', name: 'French (France)', nativeName: 'Français (France)' },
66
+ { code: 'fr-CA', name: 'French (Canada)', nativeName: 'Français (Canada)' },
67
+ { code: 'fr-BE', name: 'French (Belgium)', nativeName: 'Français (Belgique)' },
68
+ { code: 'fr-CH', name: 'French (Switzerland)', nativeName: 'Français (Suisse)' },
69
+ { code: 'fr-LU', name: 'French (Luxembourg)', nativeName: 'Français (Luxembourg)' },
70
+ { code: 'fy', name: 'Frisian', nativeName: 'Frysk' },
71
+
72
+ // ── G ──────────────────────────────────────────────────────────────────────
73
+ { code: 'gl', name: 'Galician', nativeName: 'Galego' },
74
+ { code: 'ka', name: 'Georgian', nativeName: 'ქართული' },
75
+ { code: 'de', name: 'German', nativeName: 'Deutsch' },
76
+ { code: 'de-DE', name: 'German (Germany)', nativeName: 'Deutsch (Deutschland)' },
77
+ { code: 'de-AT', name: 'German (Austria)', nativeName: 'Deutsch (Österreich)' },
78
+ { code: 'de-CH', name: 'German (Switzerland)', nativeName: 'Deutsch (Schweiz)' },
79
+ { code: 'de-LU', name: 'German (Luxembourg)', nativeName: 'Deutsch (Luxemburg)' },
80
+ { code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
81
+ { code: 'gu', name: 'Gujarati', nativeName: 'ગુજરાતી' },
82
+
83
+ // ── H ──────────────────────────────────────────────────────────────────────
84
+ { code: 'ht', name: 'Haitian Creole', nativeName: 'Kreyòl ayisyen' },
85
+ { code: 'ha', name: 'Hausa', nativeName: 'Hausa' },
86
+ { code: 'haw', name: 'Hawaiian', nativeName: 'ʻŌlelo Hawaiʻi' },
87
+ { code: 'he', name: 'Hebrew', nativeName: 'עברית' },
88
+ { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
89
+ { code: 'hmn', name: 'Hmong', nativeName: 'Hmoob' },
90
+ { code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
91
+
92
+ // ── I ──────────────────────────────────────────────────────────────────────
93
+ { code: 'is', name: 'Icelandic', nativeName: 'Íslenska' },
94
+ { code: 'ig', name: 'Igbo', nativeName: 'Igbo' },
95
+ { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia' },
96
+ { code: 'ga', name: 'Irish', nativeName: 'Gaeilge' },
97
+ { code: 'it', name: 'Italian', nativeName: 'Italiano' },
98
+ { code: 'it-IT', name: 'Italian (Italy)', nativeName: 'Italiano (Italia)' },
99
+ { code: 'it-CH', name: 'Italian (Switzerland)', nativeName: 'Italiano (Svizzera)' },
100
+
101
+ // ── J ──────────────────────────────────────────────────────────────────────
102
+ { code: 'ja', name: 'Japanese', nativeName: '日本語' },
103
+ { code: 'jv', name: 'Javanese', nativeName: 'Basa Jawa' },
104
+
105
+ // ── K ──────────────────────────────────────────────────────────────────────
106
+ { code: 'kn', name: 'Kannada', nativeName: 'ಕನ್ನಡ' },
107
+ { code: 'kk', name: 'Kazakh', nativeName: 'Қазақ' },
108
+ { code: 'km', name: 'Khmer', nativeName: 'ខ្មែរ' },
109
+ { code: 'rw', name: 'Kinyarwanda', nativeName: 'Ikinyarwanda' },
110
+ { code: 'ko', name: 'Korean', nativeName: '한국어' },
111
+ { code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
112
+ { code: 'ky', name: 'Kyrgyz', nativeName: 'Кыргызча' },
113
+
114
+ // ── L ──────────────────────────────────────────────────────────────────────
115
+ { code: 'lo', name: 'Lao', nativeName: 'ລາວ' },
116
+ { code: 'la', name: 'Latin', nativeName: 'Latina' },
117
+ { code: 'lv', name: 'Latvian', nativeName: 'Latviešu' },
118
+ { code: 'lt', name: 'Lithuanian', nativeName: 'Lietuvių' },
119
+ { code: 'lb', name: 'Luxembourgish', nativeName: 'Lëtzebuergesch' },
120
+
121
+ // ── M ──────────────────────────────────────────────────────────────────────
122
+ { code: 'mk', name: 'Macedonian', nativeName: 'Македонски' },
123
+ { code: 'mg', name: 'Malagasy', nativeName: 'Malagasy' },
124
+ { code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu' },
125
+ { code: 'ms-MY', name: 'Malay (Malaysia)', nativeName: 'Bahasa Melayu (Malaysia)' },
126
+ { code: 'ms-BN', name: 'Malay (Brunei)', nativeName: 'Bahasa Melayu (Brunei)' },
127
+ { code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
128
+ { code: 'mt', name: 'Maltese', nativeName: 'Malti' },
129
+ { code: 'mi', name: 'Maori', nativeName: 'Māori' },
130
+ { code: 'mr', name: 'Marathi', nativeName: 'मराठी' },
131
+ { code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
132
+ { code: 'my', name: 'Myanmar (Burmese)', nativeName: 'မြန်မာ' },
133
+
134
+ // ── N ──────────────────────────────────────────────────────────────────────
135
+ { code: 'ne', name: 'Nepali', nativeName: 'नेपाली' },
136
+ { code: 'nb', name: 'Norwegian Bokmål', nativeName: 'Norsk bokmål' },
137
+ { code: 'nn', name: 'Norwegian Nynorsk', nativeName: 'Norsk nynorsk' },
138
+ { code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
139
+ { code: 'ny', name: 'Nyanja (Chichewa)', nativeName: 'Nyanja' },
140
+
141
+ // ── O ──────────────────────────────────────────────────────────────────────
142
+ { code: 'or', name: 'Odia (Oriya)', nativeName: 'ଓଡ଼ିଆ' },
143
+
144
+ // ── P ──────────────────────────────────────────────────────────────────────
145
+ { code: 'ps', name: 'Pashto', nativeName: 'پښتو' },
146
+ { code: 'fa', name: 'Persian', nativeName: 'فارسی' },
147
+ { code: 'pl', name: 'Polish', nativeName: 'Polski' },
148
+ { code: 'pt', name: 'Portuguese', nativeName: 'Português' },
149
+ { code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)' },
150
+ { code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)' },
151
+ { code: 'pa', name: 'Punjabi', nativeName: 'ਪੰਜਾਬੀ' },
152
+
153
+ // ── R ──────────────────────────────────────────────────────────────────────
154
+ { code: 'ro', name: 'Romanian', nativeName: 'Română' },
155
+ { code: 'ro-RO', name: 'Romanian (Romania)', nativeName: 'Română (România)' },
156
+ { code: 'ro-MD', name: 'Romanian (Moldova)', nativeName: 'Română (Moldova)' },
157
+ { code: 'ru', name: 'Russian', nativeName: 'Русский' },
158
+
159
+ // ── S ──────────────────────────────────────────────────────────────────────
160
+ { code: 'sm', name: 'Samoan', nativeName: 'Gagana Samoa' },
161
+ { code: 'gd', name: 'Scots Gaelic', nativeName: 'Gàidhlig' },
162
+ { code: 'sr', name: 'Serbian (Cyrillic)', nativeName: 'Српски' },
163
+ { code: 'sr-Latn', name: 'Serbian (Latin)', nativeName: 'Srpski (latinica)' },
164
+ { code: 'sr-Cyrl', name: 'Serbian (Cyrillic)', nativeName: 'Српски (ћирилица)' },
165
+ { code: 'st', name: 'Sesotho', nativeName: 'Sesotho' },
166
+ { code: 'sn', name: 'Shona', nativeName: 'chiShona' },
167
+ { code: 'sd', name: 'Sindhi', nativeName: 'سنڌي' },
168
+ { code: 'si', name: 'Sinhala', nativeName: 'සිංහල' },
169
+ { code: 'sk', name: 'Slovak', nativeName: 'Slovenčina' },
170
+ { code: 'sl', name: 'Slovenian', nativeName: 'Slovenščina' },
171
+ { code: 'so', name: 'Somali', nativeName: 'Soomaali' },
172
+ { code: 'es', name: 'Spanish', nativeName: 'Español' },
173
+ { code: 'es-ES', name: 'Spanish (Spain)', nativeName: 'Español (España)' },
174
+ { code: 'es-MX', name: 'Spanish (Mexico)', nativeName: 'Español (México)' },
175
+ { code: 'es-AR', name: 'Spanish (Argentina)', nativeName: 'Español (Argentina)' },
176
+ { code: 'es-CO', name: 'Spanish (Colombia)', nativeName: 'Español (Colombia)' },
177
+ { code: 'es-CL', name: 'Spanish (Chile)', nativeName: 'Español (Chile)' },
178
+ { code: 'es-PE', name: 'Spanish (Peru)', nativeName: 'Español (Perú)' },
179
+ { code: 'es-VE', name: 'Spanish (Venezuela)', nativeName: 'Español (Venezuela)' },
180
+ { code: 'es-EC', name: 'Spanish (Ecuador)', nativeName: 'Español (Ecuador)' },
181
+ { code: 'es-BO', name: 'Spanish (Bolivia)', nativeName: 'Español (Bolivia)' },
182
+ { code: 'es-UY', name: 'Spanish (Uruguay)', nativeName: 'Español (Uruguay)' },
183
+ { code: 'es-PY', name: 'Spanish (Paraguay)', nativeName: 'Español (Paraguay)' },
184
+ { code: 'es-CR', name: 'Spanish (Costa Rica)', nativeName: 'Español (Costa Rica)' },
185
+ { code: 'es-419', name: 'Spanish (Latin America)', nativeName: 'Español (Latinoamérica)' },
186
+ { code: 'su', name: 'Sundanese', nativeName: 'Basa Sunda' },
187
+ { code: 'sw', name: 'Swahili', nativeName: 'Kiswahili' },
188
+ { code: 'sv', name: 'Swedish', nativeName: 'Svenska' },
189
+ { code: 'sv-SE', name: 'Swedish (Sweden)', nativeName: 'Svenska (Sverige)' },
190
+ { code: 'sv-FI', name: 'Swedish (Finland)', nativeName: 'Svenska (Finland)' },
191
+
192
+ // ── T ──────────────────────────────────────────────────────────────────────
193
+ { code: 'tl', name: 'Filipino (Tagalog)', nativeName: 'Filipino' },
194
+ { code: 'tg', name: 'Tajik', nativeName: 'Тоҷикӣ' },
195
+ { code: 'ta', name: 'Tamil', nativeName: 'தமிழ்' },
196
+ { code: 'tt', name: 'Tatar', nativeName: 'Татар' },
197
+ { code: 'te', name: 'Telugu', nativeName: 'తెలుగు' },
198
+ { code: 'th', name: 'Thai', nativeName: 'ภาษาไทย' },
199
+ { code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
200
+ { code: 'tk', name: 'Turkmen', nativeName: 'Türkmen' },
201
+
202
+ // ── U ──────────────────────────────────────────────────────────────────────
203
+ { code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
204
+ { code: 'ur', name: 'Urdu', nativeName: 'اردو' },
205
+ { code: 'ug', name: 'Uyghur', nativeName: 'ئۇيغۇرچە' },
206
+ { code: 'uz', name: 'Uzbek', nativeName: "O'zbek" },
207
+
208
+ // ── V ──────────────────────────────────────────────────────────────────────
209
+ { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
210
+
211
+ // ── W ──────────────────────────────────────────────────────────────────────
212
+ { code: 'cy', name: 'Welsh', nativeName: 'Cymraeg' },
213
+
214
+ // ── X ──────────────────────────────────────────────────────────────────────
215
+ { code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
216
+
217
+ // ── Y ──────────────────────────────────────────────────────────────────────
218
+ { code: 'yi', name: 'Yiddish', nativeName: 'ייִדיש' },
219
+ { code: 'yo', name: 'Yoruba', nativeName: 'Yorùbá' },
220
+
221
+ // ── Z ──────────────────────────────────────────────────────────────────────
222
+ { code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
223
+ ]