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,42 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
import { autoTranslateUtil } from '../../utils/auto-translate.util'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { project_id, code, name, is_default, fallback_code } = body
|
|
7
|
+
|
|
8
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
9
|
+
if (!code || !name) throw createError({ statusCode: 400, message: 'code and name are required' })
|
|
10
|
+
|
|
11
|
+
const db = getDb()
|
|
12
|
+
|
|
13
|
+
if (is_default) {
|
|
14
|
+
await db('languages').where({ project_id: Number(project_id) }).update({ is_default: false })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const existing = await db('languages').where({ project_id: Number(project_id), code }).first()
|
|
18
|
+
if (existing) {
|
|
19
|
+
await db('languages').where({ id: existing.id }).update({
|
|
20
|
+
name,
|
|
21
|
+
is_default: is_default || false,
|
|
22
|
+
fallback_code: fallback_code ?? existing.fallback_code,
|
|
23
|
+
})
|
|
24
|
+
return db('languages').where({ id: existing.id }).first()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [id] = await db('languages').insert({
|
|
28
|
+
project_id: Number(project_id),
|
|
29
|
+
code,
|
|
30
|
+
name,
|
|
31
|
+
is_default: is_default || false,
|
|
32
|
+
fallback_code: fallback_code || null,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// If this is the system project and the language isn't English, auto-translate UI strings
|
|
36
|
+
const project = await db('projects').where({ id: Number(project_id) }).first()
|
|
37
|
+
if (project?.is_system && code !== 'en') {
|
|
38
|
+
setImmediate(() => autoTranslateUtil(db, code))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return db('languages').where({ id }).first()
|
|
42
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getDb } from '../db/index'
|
|
2
|
+
import { autoTranslateUtil } from '../utils/auto-translate.util'
|
|
3
|
+
|
|
4
|
+
// Saves onboarding configuration: UI languages for Dashboard UI project
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const body = await readBody(event)
|
|
7
|
+
const { languages, defaultLanguage } = body // languages: Array<{ code, name }>
|
|
8
|
+
|
|
9
|
+
const db = getDb()
|
|
10
|
+
const systemProject = await db('projects').where({ is_system: true }).first()
|
|
11
|
+
|
|
12
|
+
const langsToTranslate: string[] = []
|
|
13
|
+
|
|
14
|
+
if (systemProject && languages?.length) {
|
|
15
|
+
const existingLangs = await db('languages').where({ project_id: systemProject.id }).select('code')
|
|
16
|
+
const existingCodes = existingLangs.map((l: any) => l.code)
|
|
17
|
+
|
|
18
|
+
for (const lang of languages) {
|
|
19
|
+
if (!existingCodes.includes(lang.code)) {
|
|
20
|
+
await db('languages').insert({
|
|
21
|
+
project_id: systemProject.id,
|
|
22
|
+
code: lang.code,
|
|
23
|
+
name: lang.name,
|
|
24
|
+
is_default: lang.code === defaultLanguage,
|
|
25
|
+
})
|
|
26
|
+
// Queue translation for non-English new languages
|
|
27
|
+
if (lang.code !== 'en') {
|
|
28
|
+
langsToTranslate.push(lang.code)
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
await db('languages')
|
|
32
|
+
.where({ project_id: systemProject.id, code: lang.code })
|
|
33
|
+
.update({ is_default: lang.code === defaultLanguage })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Mark onboarding as completed (upsert to handle missing row)
|
|
39
|
+
const existing = await db('settings').where({ key: 'onboarding_completed' }).first()
|
|
40
|
+
if (existing) {
|
|
41
|
+
await db('settings').where({ key: 'onboarding_completed' }).update({ value: 'true' })
|
|
42
|
+
} else {
|
|
43
|
+
await db('settings').insert({ key: 'onboarding_completed', value: 'true' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fire-and-forget: translate UI strings for each new non-EN language
|
|
47
|
+
if (langsToTranslate.length) {
|
|
48
|
+
setImmediate(async () => {
|
|
49
|
+
for (const code of langsToTranslate) {
|
|
50
|
+
await autoTranslateUtil(db, code)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { success: true }
|
|
56
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getDb } from '../db/index'
|
|
2
|
+
import type { UserProfile } from '../interfaces/profile.interface'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
5
|
+
const user = event.context.user
|
|
6
|
+
const db = getDb()
|
|
7
|
+
|
|
8
|
+
// ── Roles with project info ───────────────────────────────────────────────
|
|
9
|
+
const roles = await db('user_project_roles as upr')
|
|
10
|
+
.leftJoin('projects as p', 'p.id', 'upr.project_id')
|
|
11
|
+
.where('upr.user_id', user.id)
|
|
12
|
+
.select('upr.role', 'upr.project_id', 'p.name as project_name', 'p.color as project_color')
|
|
13
|
+
|
|
14
|
+
// ── Languages accessible to the user ─────────────────────────────────────
|
|
15
|
+
const projectIds = roles.filter((r: any) => r.project_id).map((r: any) => r.project_id)
|
|
16
|
+
|
|
17
|
+
let languages: any[]
|
|
18
|
+
if (user.is_super_admin) {
|
|
19
|
+
languages = await db('languages as l')
|
|
20
|
+
.join('projects as p', 'p.id', 'l.project_id')
|
|
21
|
+
.where('p.is_system', false)
|
|
22
|
+
.select('l.*', 'p.name as project_name', 'p.color as project_color')
|
|
23
|
+
} else if (projectIds.length) {
|
|
24
|
+
languages = await db('languages as l')
|
|
25
|
+
.join('projects as p', 'p.id', 'l.project_id')
|
|
26
|
+
.whereIn('l.project_id', projectIds)
|
|
27
|
+
.where('p.is_system', false)
|
|
28
|
+
.select('l.*', 'p.name as project_name', 'p.color as project_color')
|
|
29
|
+
} else {
|
|
30
|
+
languages = []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Stats ─────────────────────────────────────────────────────────────────
|
|
34
|
+
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
35
|
+
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
36
|
+
|
|
37
|
+
const [countTotal, countWeek, countMonth] = await Promise.all([
|
|
38
|
+
db('translation_history').where('changed_by', user.name).count('id as n').first(),
|
|
39
|
+
db('translation_history').where('changed_by', user.name).where('changed_at', '>=', weekAgo).count('id as n').first(),
|
|
40
|
+
db('translation_history').where('changed_by', user.name).where('changed_at', '>=', monthAgo).count('id as n').first(),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
// ── Recent translations ───────────────────────────────────────────────────
|
|
44
|
+
const recentTranslations = await db('translation_history as th')
|
|
45
|
+
.join('translations as t', 't.id', 'th.translation_id')
|
|
46
|
+
.join('translation_keys as tk', 'tk.id', 't.key_id')
|
|
47
|
+
.join('projects as p', 'p.id', 'tk.project_id')
|
|
48
|
+
.where('th.changed_by', user.name)
|
|
49
|
+
.orderBy('th.changed_at', 'desc')
|
|
50
|
+
.limit(20)
|
|
51
|
+
.select(
|
|
52
|
+
'th.id',
|
|
53
|
+
'th.new_value',
|
|
54
|
+
'th.old_value',
|
|
55
|
+
'th.changed_at',
|
|
56
|
+
'tk.key',
|
|
57
|
+
'tk.id as key_id',
|
|
58
|
+
't.language_code',
|
|
59
|
+
'p.name as project_name',
|
|
60
|
+
'p.color as project_color',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
user: {
|
|
65
|
+
id: user.id,
|
|
66
|
+
name: user.name,
|
|
67
|
+
email: user.email,
|
|
68
|
+
is_super_admin: !!user.is_super_admin,
|
|
69
|
+
last_login_at: user.last_login_at ?? null,
|
|
70
|
+
created_at: user.created_at,
|
|
71
|
+
},
|
|
72
|
+
roles,
|
|
73
|
+
stats: {
|
|
74
|
+
total: Number(countTotal?.n ?? 0),
|
|
75
|
+
thisWeek: Number(countWeek?.n ?? 0),
|
|
76
|
+
thisMonth: Number(countMonth?.n ?? 0),
|
|
77
|
+
},
|
|
78
|
+
languages,
|
|
79
|
+
recentTranslations,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getDb } from '../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const query = getQuery(event)
|
|
5
|
+
const projectId = query.project_id ? Number(query.project_id) : null
|
|
6
|
+
|
|
7
|
+
if (!projectId) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
8
|
+
|
|
9
|
+
const db = getDb()
|
|
10
|
+
const project = await db('projects').where({ id: projectId }).first()
|
|
11
|
+
if (!project) throw createError({ statusCode: 404, message: 'Project not found' })
|
|
12
|
+
if (project.is_system) throw createError({ statusCode: 403, message: 'System project cannot be exported' })
|
|
13
|
+
|
|
14
|
+
const languages = await db('languages')
|
|
15
|
+
.where({ project_id: projectId })
|
|
16
|
+
.select('code', 'name', 'is_default', 'fallback_code')
|
|
17
|
+
|
|
18
|
+
const keyRows = await db('translation_keys')
|
|
19
|
+
.where({ project_id: projectId })
|
|
20
|
+
.select('id', 'key', 'description')
|
|
21
|
+
|
|
22
|
+
const translationRows = await db('translations as t')
|
|
23
|
+
.join('translation_keys as k', 't.key_id', 'k.id')
|
|
24
|
+
.where('k.project_id', projectId)
|
|
25
|
+
.select('k.key', 't.language_code', 't.value', 't.status')
|
|
26
|
+
|
|
27
|
+
// Build keys map: key → { description, translations: { lang: { value, status } } }
|
|
28
|
+
const keysMap: Record<string, { description: string | null; translations: Record<string, { value: string; status: string }> }> = {}
|
|
29
|
+
|
|
30
|
+
for (const row of keyRows) {
|
|
31
|
+
keysMap[row.key] = { description: row.description ?? null, translations: {} }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const row of translationRows) {
|
|
35
|
+
if (!keysMap[row.key]) continue
|
|
36
|
+
if (row.value !== null && row.value !== '') {
|
|
37
|
+
keysMap[row.key].translations[row.language_code] = {
|
|
38
|
+
value: row.value,
|
|
39
|
+
status: row.status ?? 'draft',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const snapshot = {
|
|
45
|
+
version: 1,
|
|
46
|
+
exportedAt: new Date().toISOString(),
|
|
47
|
+
project: {
|
|
48
|
+
name: project.name,
|
|
49
|
+
locales_path: project.locales_path,
|
|
50
|
+
key_separator: project.key_separator,
|
|
51
|
+
color: project.color ?? null,
|
|
52
|
+
description: project.description ?? null,
|
|
53
|
+
source_url: project.source_url ?? null,
|
|
54
|
+
},
|
|
55
|
+
languages: languages.map(l => ({
|
|
56
|
+
code: l.code,
|
|
57
|
+
name: l.name,
|
|
58
|
+
is_default: !!l.is_default,
|
|
59
|
+
fallback_code: l.fallback_code ?? null,
|
|
60
|
+
})),
|
|
61
|
+
keys: Object.entries(keysMap).map(([key, data]) => ({
|
|
62
|
+
key,
|
|
63
|
+
description: data.description,
|
|
64
|
+
translations: data.translations,
|
|
65
|
+
})),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const filename = `${project.name.replace(/[^a-z0-9]/gi, '_')}_snapshot.json`
|
|
69
|
+
setHeader(event, 'Content-Type', 'application/json')
|
|
70
|
+
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
|
|
71
|
+
|
|
72
|
+
return snapshot
|
|
73
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getDb } from '../db/index'
|
|
2
|
+
|
|
3
|
+
interface SnapshotKey {
|
|
4
|
+
key: string
|
|
5
|
+
description?: string | null
|
|
6
|
+
translations: Record<string, { value: string; status?: string } | string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SnapshotLanguage {
|
|
10
|
+
code: string
|
|
11
|
+
name: string
|
|
12
|
+
is_default?: boolean
|
|
13
|
+
fallback_code?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ProjectSnapshot {
|
|
17
|
+
version: number
|
|
18
|
+
project: {
|
|
19
|
+
name: string
|
|
20
|
+
locales_path: string
|
|
21
|
+
key_separator: string
|
|
22
|
+
color?: string | null
|
|
23
|
+
description?: string | null
|
|
24
|
+
source_url?: string | null
|
|
25
|
+
}
|
|
26
|
+
languages: SnapshotLanguage[]
|
|
27
|
+
keys: SnapshotKey[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default defineEventHandler(async (event) => {
|
|
31
|
+
const body = await readBody(event)
|
|
32
|
+
const { snapshot, project_id, mode = 'merge' } = body as {
|
|
33
|
+
snapshot: ProjectSnapshot
|
|
34
|
+
project_id?: number
|
|
35
|
+
mode?: 'merge' | 'replace'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!snapshot?.version || !snapshot?.project || !Array.isArray(snapshot.keys)) {
|
|
39
|
+
throw createError({ statusCode: 400, message: 'Invalid snapshot format' })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const db = getDb()
|
|
43
|
+
let projectId = project_id ? Number(project_id) : null
|
|
44
|
+
let stats = { keys_added: 0, keys_updated: 0, translations_added: 0, translations_updated: 0, languages_added: 0 }
|
|
45
|
+
|
|
46
|
+
// ── Resolve or create project ──────────────────────────────────────────
|
|
47
|
+
if (projectId) {
|
|
48
|
+
const existing = await db('projects').where({ id: projectId }).first()
|
|
49
|
+
if (!existing) throw createError({ statusCode: 404, message: 'Target project not found' })
|
|
50
|
+
// Update project metadata
|
|
51
|
+
await db('projects').where({ id: projectId }).update({
|
|
52
|
+
name: snapshot.project.name,
|
|
53
|
+
locales_path: snapshot.project.locales_path,
|
|
54
|
+
key_separator: snapshot.project.key_separator,
|
|
55
|
+
color: snapshot.project.color ?? existing.color,
|
|
56
|
+
description: snapshot.project.description ?? existing.description,
|
|
57
|
+
source_url: snapshot.project.source_url ?? existing.source_url,
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
// Create new project
|
|
61
|
+
const [id] = await db('projects').insert({
|
|
62
|
+
name: snapshot.project.name,
|
|
63
|
+
locales_path: snapshot.project.locales_path,
|
|
64
|
+
key_separator: snapshot.project.key_separator,
|
|
65
|
+
color: snapshot.project.color ?? '#6366f1',
|
|
66
|
+
description: snapshot.project.description ?? null,
|
|
67
|
+
source_url: snapshot.project.source_url ?? null,
|
|
68
|
+
is_system: false,
|
|
69
|
+
})
|
|
70
|
+
projectId = id
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const separator = snapshot.project.key_separator || '.'
|
|
74
|
+
|
|
75
|
+
// ── Upsert languages ──────────────────────────────────────────────────
|
|
76
|
+
for (const lang of snapshot.languages) {
|
|
77
|
+
const existing = await db('languages').where({ project_id: projectId, code: lang.code }).first()
|
|
78
|
+
if (existing) {
|
|
79
|
+
await db('languages').where({ id: existing.id }).update({
|
|
80
|
+
name: lang.name,
|
|
81
|
+
is_default: lang.is_default ? 1 : 0,
|
|
82
|
+
fallback_code: lang.fallback_code ?? null,
|
|
83
|
+
})
|
|
84
|
+
} else {
|
|
85
|
+
await db('languages').insert({
|
|
86
|
+
project_id: projectId,
|
|
87
|
+
code: lang.code,
|
|
88
|
+
name: lang.name,
|
|
89
|
+
is_default: lang.is_default ? 1 : 0,
|
|
90
|
+
fallback_code: lang.fallback_code ?? null,
|
|
91
|
+
})
|
|
92
|
+
stats.languages_added++
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Replace mode: clear existing keys ────────────────────────────────
|
|
97
|
+
if (mode === 'replace') {
|
|
98
|
+
const keyIds = await db('translation_keys').where({ project_id: projectId }).pluck('id')
|
|
99
|
+
if (keyIds.length) {
|
|
100
|
+
await db('translations').whereIn('key_id', keyIds).delete()
|
|
101
|
+
await db('key_usages').whereIn('key_id', keyIds).delete()
|
|
102
|
+
await db('translation_keys').where({ project_id: projectId }).delete()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Upsert keys + translations ────────────────────────────────────────
|
|
107
|
+
for (const entry of snapshot.keys) {
|
|
108
|
+
if (!entry.key) continue
|
|
109
|
+
|
|
110
|
+
let keyRecord = await db('translation_keys').where({ project_id: projectId, key: entry.key }).first()
|
|
111
|
+
if (!keyRecord) {
|
|
112
|
+
const [id] = await db('translation_keys').insert({
|
|
113
|
+
project_id: projectId,
|
|
114
|
+
key: entry.key,
|
|
115
|
+
description: entry.description ?? null,
|
|
116
|
+
})
|
|
117
|
+
keyRecord = { id }
|
|
118
|
+
stats.keys_added++
|
|
119
|
+
} else if (entry.description !== undefined) {
|
|
120
|
+
await db('translation_keys').where({ id: keyRecord.id }).update({ description: entry.description })
|
|
121
|
+
stats.keys_updated++
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const [langCode, translationData] of Object.entries(entry.translations)) {
|
|
125
|
+
const value = typeof translationData === 'string' ? translationData : translationData.value
|
|
126
|
+
const status = typeof translationData === 'object' ? (translationData.status ?? 'draft') : 'draft'
|
|
127
|
+
if (!value) continue
|
|
128
|
+
|
|
129
|
+
const existing = await db('translations').where({ key_id: keyRecord.id, language_code: langCode }).first()
|
|
130
|
+
if (existing) {
|
|
131
|
+
if (existing.value !== value) {
|
|
132
|
+
await db('translation_history').insert({
|
|
133
|
+
translation_id: existing.id,
|
|
134
|
+
old_value: existing.value,
|
|
135
|
+
new_value: value,
|
|
136
|
+
changed_by: 'snapshot-import',
|
|
137
|
+
})
|
|
138
|
+
await db('translations').where({ id: existing.id }).update({ value, status, updated_at: db.fn.now() })
|
|
139
|
+
stats.translations_updated++
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const [id] = await db('translations').insert({
|
|
143
|
+
key_id: keyRecord.id,
|
|
144
|
+
language_code: langCode,
|
|
145
|
+
value,
|
|
146
|
+
status,
|
|
147
|
+
})
|
|
148
|
+
await db('translation_history').insert({
|
|
149
|
+
translation_id: id,
|
|
150
|
+
old_value: null,
|
|
151
|
+
new_value: value,
|
|
152
|
+
changed_by: 'snapshot-import',
|
|
153
|
+
})
|
|
154
|
+
stats.translations_added++
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { success: true, project_id: projectId, stats }
|
|
160
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
5
|
+
const db = getDb()
|
|
6
|
+
|
|
7
|
+
const project = await db('projects').where({ id }).first()
|
|
8
|
+
if (!project) throw createError({ statusCode: 404, message: 'Project not found' })
|
|
9
|
+
if (project.is_system) throw createError({ statusCode: 403, message: 'Le projet système ne peut pas être supprimé' })
|
|
10
|
+
|
|
11
|
+
await db('projects').where({ id }).delete()
|
|
12
|
+
return { success: true }
|
|
13
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
7
|
+
const body = await readBody(event)
|
|
8
|
+
const { name, root_path, source_url, locales_path, key_separator, color, description, enable_number_formats, enable_datetime_formats, enable_modifiers } = body
|
|
9
|
+
|
|
10
|
+
const db = getDb()
|
|
11
|
+
const project = await db('projects').where({ id }).first()
|
|
12
|
+
if (!project) throw createError({ statusCode: 404, message: 'Project not found' })
|
|
13
|
+
if (project.is_system) throw createError({ statusCode: 403, message: 'La configuration du projet système ne peut pas être modifiée' })
|
|
14
|
+
|
|
15
|
+
const updates: Record<string, any> = {}
|
|
16
|
+
if (name !== undefined) updates.name = name.trim()
|
|
17
|
+
if (locales_path !== undefined) updates.locales_path = locales_path
|
|
18
|
+
if (key_separator !== undefined) updates.key_separator = key_separator
|
|
19
|
+
if (color !== undefined) updates.color = color
|
|
20
|
+
if (description !== undefined) updates.description = description
|
|
21
|
+
if (source_url !== undefined) updates.source_url = source_url?.trim() || null
|
|
22
|
+
if (enable_number_formats !== undefined) updates.enable_number_formats = enable_number_formats
|
|
23
|
+
if (enable_datetime_formats !== undefined) updates.enable_datetime_formats = enable_datetime_formats
|
|
24
|
+
if (enable_modifiers !== undefined) updates.enable_modifiers = enable_modifiers
|
|
25
|
+
|
|
26
|
+
if (root_path !== undefined) {
|
|
27
|
+
if (root_path.trim() === '') {
|
|
28
|
+
updates.root_path = ''
|
|
29
|
+
} else {
|
|
30
|
+
const absolutePath = resolve(root_path)
|
|
31
|
+
if (!existsSync(absolutePath)) {
|
|
32
|
+
throw createError({ statusCode: 400, message: `Path does not exist: ${absolutePath}` })
|
|
33
|
+
}
|
|
34
|
+
updates.root_path = absolutePath
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await db('projects').where({ id }).update(updates)
|
|
39
|
+
return db('projects').where({ id }).first()
|
|
40
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async () => {
|
|
4
|
+
const db = getDb()
|
|
5
|
+
const projects = await db('projects').select('*').orderBy('created_at', 'asc')
|
|
6
|
+
|
|
7
|
+
// For each project, add language count + key count
|
|
8
|
+
const withStats = await Promise.all(projects.map(async (p: any) => {
|
|
9
|
+
const keyCount = await db('translation_keys').where({ project_id: p.id }).count('* as count').first()
|
|
10
|
+
const langCount = await db('languages').where({ project_id: p.id }).count('* as count').first()
|
|
11
|
+
return {
|
|
12
|
+
...p,
|
|
13
|
+
key_count: Number((keyCount as any)?.count || 0),
|
|
14
|
+
language_count: Number((langCount as any)?.count || 0),
|
|
15
|
+
}
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
return withStats
|
|
19
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const body = await readBody(event)
|
|
7
|
+
const { name, root_path, source_url, locales_path, key_separator, color, description } = body
|
|
8
|
+
|
|
9
|
+
if (!name?.trim()) throw createError({ statusCode: 400, message: 'name is required' })
|
|
10
|
+
if (!root_path?.trim() && !source_url?.trim()) {
|
|
11
|
+
throw createError({ statusCode: 400, message: 'root_path or source_url is required' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let absolutePath = ''
|
|
15
|
+
if (root_path?.trim()) {
|
|
16
|
+
absolutePath = resolve(root_path)
|
|
17
|
+
if (!existsSync(absolutePath)) {
|
|
18
|
+
throw createError({ statusCode: 400, message: `Path does not exist: ${absolutePath}` })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const db = getDb()
|
|
23
|
+
const [id] = await db('projects').insert({
|
|
24
|
+
name: name.trim(),
|
|
25
|
+
root_path: absolutePath,
|
|
26
|
+
source_url: source_url?.trim() || null,
|
|
27
|
+
locales_path: locales_path || 'src/locales',
|
|
28
|
+
key_separator: key_separator || '.',
|
|
29
|
+
color: color || 'primary',
|
|
30
|
+
description: description || null,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return db('projects').where({ id }).first()
|
|
34
|
+
})
|