i18n-dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +715 -0
  3. package/app.vue +8 -0
  4. package/assets/css/main.css +21 -0
  5. package/assets/locales/en.json +380 -0
  6. package/bin/cli.mjs +279 -0
  7. package/components/LinkedKeyPicker.vue +135 -0
  8. package/components/PathPicker.vue +153 -0
  9. package/components/PluralEditor.vue +295 -0
  10. package/components/ScanModal.vue +153 -0
  11. package/components/TranslationHistoryModal.vue +66 -0
  12. package/components/TranslationRow.vue +541 -0
  13. package/components/dashboard/WidgetConfigModal.vue +121 -0
  14. package/components/dashboard/WidgetGrid.vue +190 -0
  15. package/components/dashboard/WidgetPicker.vue +75 -0
  16. package/components/dashboard/widgets/ActivityWidget.vue +109 -0
  17. package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
  18. package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
  19. package/components/dashboard/widgets/ReviewWidget.vue +150 -0
  20. package/components/dashboard/widgets/StatWidget.vue +133 -0
  21. package/composables/useAuth.ts +72 -0
  22. package/composables/useConfig.ts +14 -0
  23. package/composables/useDashboard.ts +89 -0
  24. package/composables/useFormats.ts +100 -0
  25. package/composables/useKeys.ts +231 -0
  26. package/composables/useLanguages.ts +221 -0
  27. package/composables/useProfile.ts +76 -0
  28. package/composables/useProject.ts +180 -0
  29. package/composables/useReview.ts +94 -0
  30. package/composables/useSettings.ts +30 -0
  31. package/composables/useStats.ts +16 -0
  32. package/composables/useT.ts +38 -0
  33. package/composables/useUsers.ts +101 -0
  34. package/composables/useWidgetData.ts +50 -0
  35. package/consts/commons.const.ts +6 -0
  36. package/consts/dashboard.const.ts +94 -0
  37. package/consts/languages.const.ts +223 -0
  38. package/enums/commons.enum.ts +7 -0
  39. package/i18n-dashboard.config.example.js +40 -0
  40. package/interfaces/commons.interface.ts +23 -0
  41. package/interfaces/job.interface.ts +10 -0
  42. package/interfaces/key.interface.ts +39 -0
  43. package/interfaces/languages.interface.ts +23 -0
  44. package/interfaces/project.interface.ts +9 -0
  45. package/interfaces/scan.interface.ts +12 -0
  46. package/interfaces/settings.interface.ts +4 -0
  47. package/interfaces/stat.interface.ts +30 -0
  48. package/interfaces/translation.interface.ts +11 -0
  49. package/interfaces/user.interface.ts +24 -0
  50. package/layouts/auth.vue +5 -0
  51. package/layouts/default.vue +327 -0
  52. package/middleware/auth.global.ts +26 -0
  53. package/nuxt.config.ts +66 -0
  54. package/package.json +89 -0
  55. package/pages/index.vue +5 -0
  56. package/pages/login.vue +74 -0
  57. package/pages/onboarding.vue +563 -0
  58. package/pages/projects/[id]/formats/datetime.vue +240 -0
  59. package/pages/projects/[id]/formats/modifiers.vue +194 -0
  60. package/pages/projects/[id]/formats/number.vue +250 -0
  61. package/pages/projects/[id]/index.vue +182 -0
  62. package/pages/projects/[id]/languages.vue +537 -0
  63. package/pages/projects/[id]/review.vue +109 -0
  64. package/pages/projects/[id]/settings.vue +515 -0
  65. package/pages/projects/[id]/translations/[keyId].vue +642 -0
  66. package/pages/projects/[id]/translations/index.vue +250 -0
  67. package/pages/projects/[id]/users.vue +276 -0
  68. package/pages/projects/index.vue +334 -0
  69. package/pages/users/[id]/profile.vue +421 -0
  70. package/pages/users/index.vue +345 -0
  71. package/plugins/loading.client.ts +3 -0
  72. package/plugins/ui-i18n.ts +6 -0
  73. package/server/api/auth/login.post.ts +28 -0
  74. package/server/api/auth/logout.post.ts +7 -0
  75. package/server/api/auth/me.get.ts +11 -0
  76. package/server/api/auth/me.put.ts +31 -0
  77. package/server/api/auth/password.put.ts +27 -0
  78. package/server/api/auth/status.get.ts +16 -0
  79. package/server/api/config.get.ts +10 -0
  80. package/server/api/dashboard/layout.get.ts +18 -0
  81. package/server/api/dashboard/layout.post.ts +18 -0
  82. package/server/api/db-config.get.ts +44 -0
  83. package/server/api/db-config.post.ts +73 -0
  84. package/server/api/export.get.ts +64 -0
  85. package/server/api/formats/datetime/[id].delete.ts +8 -0
  86. package/server/api/formats/datetime/[id].put.ts +15 -0
  87. package/server/api/formats/datetime.get.ts +11 -0
  88. package/server/api/formats/datetime.post.ts +16 -0
  89. package/server/api/formats/modifiers/[id].delete.ts +8 -0
  90. package/server/api/formats/modifiers/[id].put.ts +10 -0
  91. package/server/api/formats/modifiers.get.ts +10 -0
  92. package/server/api/formats/modifiers.post.ts +14 -0
  93. package/server/api/formats/number/[id].delete.ts +8 -0
  94. package/server/api/formats/number/[id].put.ts +15 -0
  95. package/server/api/formats/number.get.ts +11 -0
  96. package/server/api/formats/number.post.ts +16 -0
  97. package/server/api/formats/snippet.get.ts +87 -0
  98. package/server/api/fs/browse.get.ts +50 -0
  99. package/server/api/history/[translationId].get.ts +13 -0
  100. package/server/api/keys/[id].delete.ts +14 -0
  101. package/server/api/keys/[id].get.ts +41 -0
  102. package/server/api/keys/[id].patch.ts +20 -0
  103. package/server/api/keys/index.get.ts +98 -0
  104. package/server/api/keys/index.post.ts +17 -0
  105. package/server/api/languages/[code].delete.ts +15 -0
  106. package/server/api/languages/[id].put.ts +24 -0
  107. package/server/api/languages/index.get.ts +13 -0
  108. package/server/api/languages/index.post.ts +42 -0
  109. package/server/api/onboarding.post.ts +56 -0
  110. package/server/api/profile.get.ts +81 -0
  111. package/server/api/project-snapshot.get.ts +73 -0
  112. package/server/api/project-snapshot.post.ts +160 -0
  113. package/server/api/projects/[id].delete.ts +13 -0
  114. package/server/api/projects/[id].put.ts +40 -0
  115. package/server/api/projects/index.get.ts +19 -0
  116. package/server/api/projects/index.post.ts +34 -0
  117. package/server/api/scan.post.ts +165 -0
  118. package/server/api/settings/index.get.ts +9 -0
  119. package/server/api/settings/index.post.ts +20 -0
  120. package/server/api/setup.post.ts +39 -0
  121. package/server/api/stats/global.get.ts +126 -0
  122. package/server/api/stats.get.ts +70 -0
  123. package/server/api/sync.post.ts +179 -0
  124. package/server/api/translate.post.ts +52 -0
  125. package/server/api/translations/batch-translate.post.ts +121 -0
  126. package/server/api/translations/bulk-status.post.ts +24 -0
  127. package/server/api/translations/index.post.ts +62 -0
  128. package/server/api/translations/job/[id].get.ts +23 -0
  129. package/server/api/translations/status.post.ts +30 -0
  130. package/server/api/translations/translate-all.post.ts +18 -0
  131. package/server/api/ui-locale.get.ts +39 -0
  132. package/server/api/users/[id]/profile.get.ts +107 -0
  133. package/server/api/users/[id]/roles.put.ts +67 -0
  134. package/server/api/users/[id].delete.ts +36 -0
  135. package/server/api/users/[id].put.ts +43 -0
  136. package/server/api/users/index.get.ts +49 -0
  137. package/server/api/users/index.post.ts +89 -0
  138. package/server/consts/auto-translate.const.ts +2 -0
  139. package/server/consts/commons.const.ts +10 -0
  140. package/server/consts/db.const.ts +3 -0
  141. package/server/consts/scanner.const.ts +4 -0
  142. package/server/consts/translation-job.const.ts +8 -0
  143. package/server/db/index.ts +672 -0
  144. package/server/enums/auth.enum.ts +5 -0
  145. package/server/enums/translation.enum.ts +6 -0
  146. package/server/interfaces/profile.interface.ts +48 -0
  147. package/server/interfaces/project-config.interface.ts +9 -0
  148. package/server/interfaces/scanner.interface.ts +18 -0
  149. package/server/interfaces/translation-job.interface.ts +13 -0
  150. package/server/middleware/auth.ts +32 -0
  151. package/server/plugins/db.ts +6 -0
  152. package/server/routes/locale/[lang].get.ts +179 -0
  153. package/server/types/auth.type.ts +3 -0
  154. package/server/utils/auth.util.ts +89 -0
  155. package/server/utils/auto-translate.util.ts +112 -0
  156. package/server/utils/lang-api.util.ts +24 -0
  157. package/server/utils/mailer.util.ts +80 -0
  158. package/server/utils/project-config.util.ts +37 -0
  159. package/server/utils/scanner.uti.ts +307 -0
  160. package/server/utils/translation-job.util.ts +142 -0
  161. package/services/auth.service.ts +31 -0
  162. package/services/base.service.ts +140 -0
  163. package/services/job.service.ts +10 -0
  164. package/services/key.service.ts +26 -0
  165. package/services/language.service.ts +26 -0
  166. package/services/profile.service.ts +14 -0
  167. package/services/project.service.ts +23 -0
  168. package/services/scan.service.ts +14 -0
  169. package/services/settings.service.ts +14 -0
  170. package/services/stats.service.ts +11 -0
  171. package/services/translation.service.ts +36 -0
  172. package/services/user.service.ts +28 -0
  173. package/tsconfig.json +3 -0
  174. package/types/commons.type.ts +3 -0
  175. package/types/dashboard.type.ts +26 -0
  176. package/utils/config.util.ts +60 -0
@@ -0,0 +1,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,8 @@
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
+ await db('project_datetime_formats').where({ id }).delete()
7
+ return { ok: true }
8
+ })
@@ -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,8 @@
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
+ await db('project_modifiers').where({ id }).delete()
7
+ return { ok: true }
8
+ })
@@ -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,8 @@
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
+ await db('project_number_formats').where({ id }).delete()
7
+ return { ok: true }
8
+ })
@@ -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
+ })