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,64 @@
|
|
|
1
|
+
import { getDb } from '../db/index'
|
|
2
|
+
import { unflattenObject } from '#server/utils/lang-api.util'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const query = getQuery(event)
|
|
6
|
+
const projectId = query.project_id ? Number(query.project_id) : null
|
|
7
|
+
const lang = query.lang ? String(query.lang) : null
|
|
8
|
+
|
|
9
|
+
if (!projectId) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
10
|
+
|
|
11
|
+
const db = getDb()
|
|
12
|
+
const project = await db('projects').where({ id: projectId }).first()
|
|
13
|
+
if (!project) throw createError({ statusCode: 404, message: 'Project not found' })
|
|
14
|
+
|
|
15
|
+
const separator = project.key_separator || '.'
|
|
16
|
+
|
|
17
|
+
// ── Single language ────────────────────────────────────────────────────
|
|
18
|
+
if (lang) {
|
|
19
|
+
const language = await db('languages').where({ project_id: projectId, code: lang }).first()
|
|
20
|
+
if (!language) throw createError({ statusCode: 404, message: `Language '${lang}' not found` })
|
|
21
|
+
|
|
22
|
+
const rows = await db('translations as t')
|
|
23
|
+
.join('translation_keys as k', 't.key_id', 'k.id')
|
|
24
|
+
.where('t.language_code', lang)
|
|
25
|
+
.where('k.project_id', projectId)
|
|
26
|
+
.whereNotNull('t.value')
|
|
27
|
+
.select('k.key', 't.value')
|
|
28
|
+
|
|
29
|
+
const flat: Record<string, string> = {}
|
|
30
|
+
for (const row of rows) flat[row.key] = row.value
|
|
31
|
+
|
|
32
|
+
const nested = unflattenObject(flat, separator)
|
|
33
|
+
const filename = `${project.name.replace(/[^a-z0-9]/gi, '_')}_${lang}.json`
|
|
34
|
+
|
|
35
|
+
setHeader(event, 'Content-Type', 'application/json')
|
|
36
|
+
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
|
|
37
|
+
return nested
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── All languages ──────────────────────────────────────────────────────
|
|
41
|
+
const languages = await db('languages').where({ project_id: projectId }).select('code')
|
|
42
|
+
|
|
43
|
+
const combined: Record<string, any> = {}
|
|
44
|
+
|
|
45
|
+
for (const { code } of languages) {
|
|
46
|
+
const rows = await db('translations as t')
|
|
47
|
+
.join('translation_keys as k', 't.key_id', 'k.id')
|
|
48
|
+
.where('t.language_code', code)
|
|
49
|
+
.where('k.project_id', projectId)
|
|
50
|
+
.whereNotNull('t.value')
|
|
51
|
+
.select('k.key', 't.value')
|
|
52
|
+
|
|
53
|
+
const flat: Record<string, string> = {}
|
|
54
|
+
for (const row of rows) flat[row.key] = row.value
|
|
55
|
+
|
|
56
|
+
combined[code] = unflattenObject(flat, separator)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const filename = `${project.name.replace(/[^a-z0-9]/gi, '_')}_all.json`
|
|
60
|
+
|
|
61
|
+
setHeader(event, 'Content-Type', 'application/json')
|
|
62
|
+
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
|
|
63
|
+
return combined
|
|
64
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getDb } from '../../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { locale, name, options } = body
|
|
7
|
+
const db = getDb()
|
|
8
|
+
await db('project_datetime_formats').where({ id }).update({
|
|
9
|
+
locale,
|
|
10
|
+
name,
|
|
11
|
+
options: JSON.stringify(options || {}),
|
|
12
|
+
})
|
|
13
|
+
const row = await db('project_datetime_formats').where({ id }).first()
|
|
14
|
+
return { ...row, options: JSON.parse(row.options || '{}') }
|
|
15
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { project_id } = getQuery(event)
|
|
5
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
6
|
+
const db = getDb()
|
|
7
|
+
const rows = await db('project_datetime_formats')
|
|
8
|
+
.where({ project_id: Number(project_id) })
|
|
9
|
+
.orderBy('locale').orderBy('name')
|
|
10
|
+
return rows.map((r: any) => ({ ...r, options: JSON.parse(r.options || '{}') }))
|
|
11
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event)
|
|
5
|
+
const { project_id, locale, name, options } = body
|
|
6
|
+
if (!project_id || !locale || !name) throw createError({ statusCode: 400, message: 'project_id, locale and name are required' })
|
|
7
|
+
const db = getDb()
|
|
8
|
+
const [id] = await db('project_datetime_formats').insert({
|
|
9
|
+
project_id: Number(project_id),
|
|
10
|
+
locale,
|
|
11
|
+
name,
|
|
12
|
+
options: JSON.stringify(options || {}),
|
|
13
|
+
})
|
|
14
|
+
const row = await db('project_datetime_formats').where({ id }).first()
|
|
15
|
+
return { ...row, options: JSON.parse(row.options || '{}') }
|
|
16
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getDb } from '../../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { name, body: fnBody } = body
|
|
7
|
+
const db = getDb()
|
|
8
|
+
await db('project_modifiers').where({ id }).update({ name, body: fnBody })
|
|
9
|
+
return db('project_modifiers').where({ id }).first()
|
|
10
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { project_id } = getQuery(event)
|
|
5
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
6
|
+
const db = getDb()
|
|
7
|
+
return db('project_modifiers')
|
|
8
|
+
.where({ project_id: Number(project_id) })
|
|
9
|
+
.orderBy('name')
|
|
10
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event)
|
|
5
|
+
const { project_id, name, body: fnBody } = body
|
|
6
|
+
if (!project_id || !name || !fnBody) throw createError({ statusCode: 400, message: 'project_id, name and body are required' })
|
|
7
|
+
const db = getDb()
|
|
8
|
+
const [id] = await db('project_modifiers').insert({
|
|
9
|
+
project_id: Number(project_id),
|
|
10
|
+
name,
|
|
11
|
+
body: fnBody,
|
|
12
|
+
})
|
|
13
|
+
return db('project_modifiers').where({ id }).first()
|
|
14
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getDb } from '../../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { locale, name, options } = body
|
|
7
|
+
const db = getDb()
|
|
8
|
+
await db('project_number_formats').where({ id }).update({
|
|
9
|
+
locale,
|
|
10
|
+
name,
|
|
11
|
+
options: JSON.stringify(options || {}),
|
|
12
|
+
})
|
|
13
|
+
const row = await db('project_number_formats').where({ id }).first()
|
|
14
|
+
return { ...row, options: JSON.parse(row.options || '{}') }
|
|
15
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { project_id } = getQuery(event)
|
|
5
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
6
|
+
const db = getDb()
|
|
7
|
+
const rows = await db('project_number_formats')
|
|
8
|
+
.where({ project_id: Number(project_id) })
|
|
9
|
+
.orderBy('locale').orderBy('name')
|
|
10
|
+
return rows.map((r: any) => ({ ...r, options: JSON.parse(r.options || '{}') }))
|
|
11
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event)
|
|
5
|
+
const { project_id, locale, name, options } = body
|
|
6
|
+
if (!project_id || !locale || !name) throw createError({ statusCode: 400, message: 'project_id, locale and name are required' })
|
|
7
|
+
const db = getDb()
|
|
8
|
+
const [id] = await db('project_number_formats').insert({
|
|
9
|
+
project_id: Number(project_id),
|
|
10
|
+
locale,
|
|
11
|
+
name,
|
|
12
|
+
options: JSON.stringify(options || {}),
|
|
13
|
+
})
|
|
14
|
+
const row = await db('project_number_formats').where({ id }).first()
|
|
15
|
+
return { ...row, options: JSON.parse(row.options || '{}') }
|
|
16
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { project_id } = getQuery(event)
|
|
5
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
6
|
+
const db = getDb()
|
|
7
|
+
const pid = Number(project_id)
|
|
8
|
+
|
|
9
|
+
const [numRows, dtRows, modRows, languages] = await Promise.all([
|
|
10
|
+
db('project_number_formats').where({ project_id: pid }).orderBy('locale').orderBy('name'),
|
|
11
|
+
db('project_datetime_formats').where({ project_id: pid }).orderBy('locale').orderBy('name'),
|
|
12
|
+
db('project_modifiers').where({ project_id: pid }).orderBy('name'),
|
|
13
|
+
db('languages').where({ project_id: pid }).orderBy('is_default', 'desc').orderBy('code'),
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
// Build numberFormats object
|
|
17
|
+
const numberFormats: Record<string, Record<string, any>> = {}
|
|
18
|
+
for (const row of numRows) {
|
|
19
|
+
if (!numberFormats[row.locale]) numberFormats[row.locale] = {}
|
|
20
|
+
numberFormats[row.locale][row.name] = JSON.parse(row.options || '{}')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Build datetimeFormats object
|
|
24
|
+
const datetimeFormats: Record<string, Record<string, any>> = {}
|
|
25
|
+
for (const row of dtRows) {
|
|
26
|
+
if (!datetimeFormats[row.locale]) datetimeFormats[row.locale] = {}
|
|
27
|
+
datetimeFormats[row.locale][row.name] = JSON.parse(row.options || '{}')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Build modifiers
|
|
31
|
+
const modifiers: Record<string, string> = {}
|
|
32
|
+
for (const row of modRows) {
|
|
33
|
+
modifiers[row.name] = row.body
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultLang = languages.find((l: any) => l.is_default)?.code || languages[0]?.code || 'en'
|
|
37
|
+
|
|
38
|
+
// Generate snippet
|
|
39
|
+
const lines: string[] = []
|
|
40
|
+
lines.push(`import { createI18n } from 'vue-i18n'`)
|
|
41
|
+
lines.push(``)
|
|
42
|
+
|
|
43
|
+
if (Object.keys(numberFormats).length) {
|
|
44
|
+
lines.push(`const numberFormats = ${JSON.stringify(numberFormats, null, 2)}`)
|
|
45
|
+
lines.push(``)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Object.keys(datetimeFormats).length) {
|
|
49
|
+
lines.push(`const datetimeFormats = ${JSON.stringify(datetimeFormats, null, 2)}`)
|
|
50
|
+
lines.push(``)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push(`const i18n = createI18n({`)
|
|
54
|
+
lines.push(` locale: '${defaultLang}',`)
|
|
55
|
+
lines.push(` legacy: false,`)
|
|
56
|
+
|
|
57
|
+
if (Object.keys(numberFormats).length) {
|
|
58
|
+
lines.push(` numberFormats,`)
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(datetimeFormats).length) {
|
|
61
|
+
lines.push(` datetimeFormats,`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Object.keys(modifiers).length) {
|
|
65
|
+
lines.push(` modifiers: {`)
|
|
66
|
+
for (const [name, body] of Object.entries(modifiers)) {
|
|
67
|
+
lines.push(` ${name}: ${body},`)
|
|
68
|
+
}
|
|
69
|
+
lines.push(` },`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push(` messages: {`)
|
|
73
|
+
for (const lang of languages) {
|
|
74
|
+
lines.push(` ${lang.code}: await fetch('[your-dashboard-url]/locale/${lang.code}.json').then(r => r.json()),`)
|
|
75
|
+
}
|
|
76
|
+
lines.push(` },`)
|
|
77
|
+
lines.push(`})`)
|
|
78
|
+
lines.push(``)
|
|
79
|
+
lines.push(`export default i18n`)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
snippet: lines.join('\n'),
|
|
83
|
+
numberFormats,
|
|
84
|
+
datetimeFormats,
|
|
85
|
+
modifiers,
|
|
86
|
+
}
|
|
87
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync } from 'fs'
|
|
2
|
+
import { resolve, join, dirname, sep } from 'path'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const query = getQuery(event)
|
|
7
|
+
const rawPath = query.path && String(query.path).trim() ? String(query.path).trim() : homedir()
|
|
8
|
+
|
|
9
|
+
const absolutePath = resolve(rawPath)
|
|
10
|
+
|
|
11
|
+
if (!existsSync(absolutePath)) {
|
|
12
|
+
throw createError({ statusCode: 404, message: `Path not found: ${absolutePath}` })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const stat = statSync(absolutePath)
|
|
16
|
+
if (!stat.isDirectory()) {
|
|
17
|
+
throw createError({ statusCode: 400, message: 'Path is not a directory' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let entries: { name: string; path: string }[] = []
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(absolutePath, { withFileTypes: true })
|
|
23
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
24
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
25
|
+
.map(e => ({ name: e.name, path: join(absolutePath, e.name) }))
|
|
26
|
+
} catch {
|
|
27
|
+
entries = []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Build breadcrumbs from path segments
|
|
31
|
+
const parts = absolutePath.split(sep).filter(Boolean)
|
|
32
|
+
const breadcrumbs = parts.map((part, i) => ({
|
|
33
|
+
name: part || sep,
|
|
34
|
+
path: sep + parts.slice(0, i + 1).join(sep),
|
|
35
|
+
}))
|
|
36
|
+
// Add root on unix
|
|
37
|
+
if (absolutePath.startsWith(sep)) {
|
|
38
|
+
breadcrumbs.unshift({ name: sep, path: sep })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parent = absolutePath !== sep ? dirname(absolutePath) : null
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
current: absolutePath,
|
|
45
|
+
parent,
|
|
46
|
+
home: homedir(),
|
|
47
|
+
breadcrumbs,
|
|
48
|
+
entries,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const translationId = getRouterParam(event, 'translationId')
|
|
5
|
+
const db = getDb()
|
|
6
|
+
|
|
7
|
+
const history = await db('translation_history')
|
|
8
|
+
.where({ translation_id: Number(translationId) })
|
|
9
|
+
.orderBy('changed_at', 'desc')
|
|
10
|
+
.limit(50)
|
|
11
|
+
|
|
12
|
+
return history
|
|
13
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = getRouterParam(event, 'id')
|
|
5
|
+
const db = getDb()
|
|
6
|
+
|
|
7
|
+
const key = await db('translation_keys').where({ id: Number(id) }).first()
|
|
8
|
+
if (!key) {
|
|
9
|
+
throw createError({ statusCode: 404, message: 'Key not found' })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await db('translation_keys').where({ id: Number(id) }).delete()
|
|
13
|
+
return { success: true }
|
|
14
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = getRouterParam(event, 'id')
|
|
5
|
+
const db = getDb()
|
|
6
|
+
|
|
7
|
+
const key = await db('translation_keys').where({ id: Number(id) }).first()
|
|
8
|
+
if (!key) throw createError({ statusCode: 404, message: 'Key not found' })
|
|
9
|
+
|
|
10
|
+
const languages = await db('languages')
|
|
11
|
+
.where({ project_id: key.project_id })
|
|
12
|
+
.orderBy('is_default', 'desc')
|
|
13
|
+
.orderBy('name', 'asc')
|
|
14
|
+
|
|
15
|
+
const translations = await db('translations')
|
|
16
|
+
.where({ key_id: Number(id) })
|
|
17
|
+
.select('*')
|
|
18
|
+
|
|
19
|
+
const usages = await db('key_usages')
|
|
20
|
+
.where({ key_id: Number(id) })
|
|
21
|
+
.select('*')
|
|
22
|
+
.orderBy('file_path', 'asc')
|
|
23
|
+
.orderBy('line_number', 'asc')
|
|
24
|
+
|
|
25
|
+
// Build translations map + fetch history per translation
|
|
26
|
+
const translationMap: Record<string, any> = {}
|
|
27
|
+
for (const tr of translations) {
|
|
28
|
+
const history = await db('translation_history')
|
|
29
|
+
.where({ translation_id: tr.id })
|
|
30
|
+
.orderBy('changed_at', 'desc')
|
|
31
|
+
.limit(20)
|
|
32
|
+
translationMap[tr.language_code] = { ...tr, history }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
...key,
|
|
37
|
+
languages,
|
|
38
|
+
translations: translationMap,
|
|
39
|
+
usages,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = getRouterParam(event, 'id')
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { description } = body
|
|
7
|
+
|
|
8
|
+
const db = getDb()
|
|
9
|
+
|
|
10
|
+
const key = await db('translation_keys').where({ id: Number(id) }).first()
|
|
11
|
+
if (!key) {
|
|
12
|
+
throw createError({ statusCode: 404, message: 'Key not found' })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await db('translation_keys')
|
|
16
|
+
.where({ id: Number(id) })
|
|
17
|
+
.update({ description: description ?? null })
|
|
18
|
+
|
|
19
|
+
return db('translation_keys').where({ id: Number(id) }).first()
|
|
20
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const query = getQuery(event)
|
|
5
|
+
const { project_id, search, page = 1, limit = 50, lang, status } = query
|
|
6
|
+
|
|
7
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
8
|
+
|
|
9
|
+
const db = getDb()
|
|
10
|
+
const offset = (Number(page) - 1) * Number(limit)
|
|
11
|
+
|
|
12
|
+
const languages = await db('languages')
|
|
13
|
+
.where({ project_id: Number(project_id) })
|
|
14
|
+
.select('*')
|
|
15
|
+
.orderBy('is_default', 'desc')
|
|
16
|
+
.orderBy('name', 'asc')
|
|
17
|
+
|
|
18
|
+
let keysQuery = db('translation_keys as tk')
|
|
19
|
+
.where('tk.project_id', Number(project_id))
|
|
20
|
+
.select('tk.*')
|
|
21
|
+
.orderBy('tk.key', 'asc')
|
|
22
|
+
|
|
23
|
+
if (search) {
|
|
24
|
+
keysQuery = keysQuery.where('tk.key', 'like', `%${search}%`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (status === 'unused') {
|
|
28
|
+
keysQuery = keysQuery.where('tk.is_unused', true)
|
|
29
|
+
} else if (status === 'missing' && lang && lang !== 'all') {
|
|
30
|
+
keysQuery = keysQuery.whereNotExists(
|
|
31
|
+
db('translations as t')
|
|
32
|
+
.where('t.key_id', db.ref('tk.id'))
|
|
33
|
+
.where('t.language_code', lang as string)
|
|
34
|
+
.whereNotNull('t.value')
|
|
35
|
+
.where('t.value', '!=', '')
|
|
36
|
+
.select('t.id'),
|
|
37
|
+
)
|
|
38
|
+
} else if (lang && lang !== 'all') {
|
|
39
|
+
if (status && status !== 'all') {
|
|
40
|
+
keysQuery = keysQuery.whereExists(
|
|
41
|
+
db('translations as t')
|
|
42
|
+
.where('t.key_id', db.ref('tk.id'))
|
|
43
|
+
.where('t.language_code', lang as string)
|
|
44
|
+
.where('t.status', status as string)
|
|
45
|
+
.select('t.id'),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
} else if (status && status !== 'all' && status !== 'unused') {
|
|
49
|
+
keysQuery = keysQuery.whereExists(
|
|
50
|
+
db('translations as t')
|
|
51
|
+
.where('t.key_id', db.ref('tk.id'))
|
|
52
|
+
.where('t.status', status as string)
|
|
53
|
+
.select('t.id'),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const totalQuery = keysQuery.clone().clearOrder().clearSelect().count('* as count')
|
|
58
|
+
const [{ count }] = await totalQuery
|
|
59
|
+
const total = Number(count)
|
|
60
|
+
|
|
61
|
+
const keys = await keysQuery.offset(offset).limit(Number(limit))
|
|
62
|
+
const keyIds = keys.map((k: any) => k.id)
|
|
63
|
+
|
|
64
|
+
let translations: any[] = []
|
|
65
|
+
if (keyIds.length > 0) {
|
|
66
|
+
translations = await db('translations as t')
|
|
67
|
+
.whereIn('t.key_id', keyIds)
|
|
68
|
+
.select('t.*')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let usages: any[] = []
|
|
72
|
+
if (keyIds.length > 0) {
|
|
73
|
+
usages = await db('key_usages')
|
|
74
|
+
.whereIn('key_id', keyIds)
|
|
75
|
+
.select('key_id', 'file_path', 'line_number', 'detected_function')
|
|
76
|
+
.orderBy('file_path', 'asc')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const translationMap: Record<number, Record<string, any>> = {}
|
|
80
|
+
for (const tr of translations) {
|
|
81
|
+
if (!translationMap[tr.key_id]) translationMap[tr.key_id] = {}
|
|
82
|
+
translationMap[tr.key_id][tr.language_code] = tr
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const usageMap: Record<number, any[]> = {}
|
|
86
|
+
for (const u of usages) {
|
|
87
|
+
if (!usageMap[u.key_id]) usageMap[u.key_id] = []
|
|
88
|
+
usageMap[u.key_id].push(u)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = keys.map((key: any) => ({
|
|
92
|
+
...key,
|
|
93
|
+
translations: translationMap[key.id] || {},
|
|
94
|
+
usages: usageMap[key.id] || [],
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
return { data: result, total, page: Number(page), limit: Number(limit), languages }
|
|
98
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event)
|
|
5
|
+
const { project_id, key, description } = body
|
|
6
|
+
|
|
7
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
8
|
+
if (!key) throw createError({ statusCode: 400, message: 'key is required' })
|
|
9
|
+
|
|
10
|
+
const db = getDb()
|
|
11
|
+
|
|
12
|
+
const existing = await db('translation_keys').where({ project_id: Number(project_id), key }).first()
|
|
13
|
+
if (existing) throw createError({ statusCode: 409, message: 'Key already exists' })
|
|
14
|
+
|
|
15
|
+
const [id] = await db('translation_keys').insert({ project_id: Number(project_id), key, description })
|
|
16
|
+
return db('translation_keys').where({ id }).first()
|
|
17
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const code = getRouterParam(event, 'code')
|
|
5
|
+
const { project_id } = getQuery(event)
|
|
6
|
+
|
|
7
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
8
|
+
|
|
9
|
+
const db = getDb()
|
|
10
|
+
const language = await db('languages').where({ project_id: Number(project_id), code }).first()
|
|
11
|
+
if (!language) throw createError({ statusCode: 404, message: 'Language not found' })
|
|
12
|
+
|
|
13
|
+
await db('languages').where({ id: language.id }).delete()
|
|
14
|
+
return { success: true }
|
|
15
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
const { fallback_code, name, is_default } = body
|
|
7
|
+
|
|
8
|
+
const db = getDb()
|
|
9
|
+
const lang = await db('languages').where({ id }).first()
|
|
10
|
+
if (!lang) throw createError({ statusCode: 404, message: 'Language not found' })
|
|
11
|
+
|
|
12
|
+
if (is_default) {
|
|
13
|
+
await db('languages').where({ project_id: lang.project_id }).update({ is_default: false })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const updates: Record<string, any> = {}
|
|
17
|
+
if (name !== undefined) updates.name = name
|
|
18
|
+
if (is_default !== undefined) updates.is_default = is_default
|
|
19
|
+
// fallback_code: null = aucun fallback explicite (auto BCP 47 prend le relais)
|
|
20
|
+
if ('fallback_code' in body) updates.fallback_code = fallback_code || null
|
|
21
|
+
|
|
22
|
+
await db('languages').where({ id }).update(updates)
|
|
23
|
+
return db('languages').where({ id }).first()
|
|
24
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getDb } from '../../db/index'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { project_id } = getQuery(event)
|
|
5
|
+
if (!project_id) throw createError({ statusCode: 400, message: 'project_id is required' })
|
|
6
|
+
|
|
7
|
+
const db = getDb()
|
|
8
|
+
return db('languages')
|
|
9
|
+
.where({ project_id: Number(project_id) })
|
|
10
|
+
.select('*')
|
|
11
|
+
.orderBy('is_default', 'desc')
|
|
12
|
+
.orderBy('name', 'asc')
|
|
13
|
+
})
|