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,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,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
|
+
]
|