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,672 @@
|
|
|
1
|
+
import knex, { type Knex } from 'knex'
|
|
2
|
+
import { resolve, basename } from 'path'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
4
|
+
import { useRuntimeConfig } from '#imports'
|
|
5
|
+
|
|
6
|
+
import { OVERRIDE_FILE } from '../consts/db.const'
|
|
7
|
+
import { _dirname } from '../../consts/commons.const'
|
|
8
|
+
|
|
9
|
+
let _db: Knex | null = null
|
|
10
|
+
|
|
11
|
+
function readOverride(): Record<string, string> | null {
|
|
12
|
+
if (existsSync(OVERRIDE_FILE)) {
|
|
13
|
+
try { return JSON.parse(readFileSync(OVERRIDE_FILE, 'utf-8')) } catch { /* ignore */ }
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildConnectionFromParams(params: {
|
|
19
|
+
dbClient: string
|
|
20
|
+
dbConnection?: string
|
|
21
|
+
dbHost?: string
|
|
22
|
+
dbPort?: string
|
|
23
|
+
dbUser?: string
|
|
24
|
+
dbPassword?: string
|
|
25
|
+
dbName?: string
|
|
26
|
+
}): Knex.Config {
|
|
27
|
+
const client = params.dbClient || 'better-sqlite3'
|
|
28
|
+
|
|
29
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
30
|
+
const dbPath = params.dbConnection || './i18n-dashboard.db'
|
|
31
|
+
const resolvedPath = dbPath.startsWith('.') ? resolve(process.cwd(), dbPath) : dbPath
|
|
32
|
+
return {
|
|
33
|
+
client: 'better-sqlite3',
|
|
34
|
+
connection: { filename: resolvedPath },
|
|
35
|
+
useNullAsDefault: true,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (client === 'pg' || client === 'postgresql') {
|
|
40
|
+
return {
|
|
41
|
+
client: 'pg',
|
|
42
|
+
connection: {
|
|
43
|
+
host: params.dbHost,
|
|
44
|
+
port: parseInt(params.dbPort || '5432'),
|
|
45
|
+
user: params.dbUser,
|
|
46
|
+
password: params.dbPassword,
|
|
47
|
+
database: params.dbName,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (client === 'mysql2' || client === 'mysql') {
|
|
53
|
+
return {
|
|
54
|
+
client: 'mysql2',
|
|
55
|
+
connection: {
|
|
56
|
+
host: params.dbHost,
|
|
57
|
+
port: parseInt(params.dbPort || '3306'),
|
|
58
|
+
user: params.dbUser,
|
|
59
|
+
password: params.dbPassword,
|
|
60
|
+
database: params.dbName,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Unsupported database client: ${client}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildConnection(config: ReturnType<typeof useRuntimeConfig>): Knex.Config {
|
|
69
|
+
const override = readOverride()
|
|
70
|
+
const params = {
|
|
71
|
+
dbClient: (override?.dbClient || config.dbClient || 'better-sqlite3') as string,
|
|
72
|
+
dbConnection: (override?.dbConnection || config.dbConnection) as string | undefined,
|
|
73
|
+
dbHost: (override?.dbHost || config.dbHost) as string | undefined,
|
|
74
|
+
dbPort: (override?.dbPort || config.dbPort) as string | undefined,
|
|
75
|
+
dbUser: (override?.dbUser || config.dbUser) as string | undefined,
|
|
76
|
+
dbPassword: (override?.dbPassword || config.dbPassword) as string | undefined,
|
|
77
|
+
dbName: (override?.dbName || config.dbName) as string | undefined,
|
|
78
|
+
}
|
|
79
|
+
return buildConnectionFromParams(params)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getDb(): Knex {
|
|
83
|
+
if (_db) return _db
|
|
84
|
+
const config = useRuntimeConfig()
|
|
85
|
+
_db = knex(buildConnection(config))
|
|
86
|
+
return _db
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function resetDb(knexConfig: Knex.Config): Promise<void> {
|
|
90
|
+
if (_db) {
|
|
91
|
+
await _db.destroy()
|
|
92
|
+
_db = null
|
|
93
|
+
}
|
|
94
|
+
_db = knex(knexConfig)
|
|
95
|
+
await initDb()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function saveDbOverride(override: Record<string, string>): void {
|
|
99
|
+
writeFileSync(OVERRIDE_FILE, JSON.stringify(override, null, 2))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { buildConnectionFromParams }
|
|
103
|
+
|
|
104
|
+
// Helper: safely add a column only if it doesn't exist yet
|
|
105
|
+
async function addColumnIfMissing(db: Knex, tableName: string, columnName: string, addFn: (table: any) => void) {
|
|
106
|
+
try {
|
|
107
|
+
const exists = await db.schema.hasColumn(tableName, columnName)
|
|
108
|
+
if (!exists) {
|
|
109
|
+
await db.schema.table(tableName, addFn)
|
|
110
|
+
}
|
|
111
|
+
} catch (e: any) {
|
|
112
|
+
if (!e.message?.includes('duplicate column') && !e.message?.includes('already exists')) {
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reads a dashboard-ui locale JSON file bundled with the package
|
|
119
|
+
function readLocaleFile(lang: string): Record<string, string> {
|
|
120
|
+
const candidates = [
|
|
121
|
+
resolve(process.cwd(), `assets/locales/${lang}.json`),
|
|
122
|
+
resolve(_dirname, `../../assets/locales/${lang}.json`),
|
|
123
|
+
resolve(_dirname, `../assets/locales/${lang}.json`),
|
|
124
|
+
]
|
|
125
|
+
for (const p of candidates) {
|
|
126
|
+
if (existsSync(p)) {
|
|
127
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')) } catch { /* ignore */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Dashboard UI strings seeded into the system project (FR default + EN)
|
|
134
|
+
const DASHBOARD_UI_STRINGS: Array<{ key: string; fr: string; en: string }> = [
|
|
135
|
+
// Navigation
|
|
136
|
+
{ key: 'nav.dashboard', fr: 'Dashboard', en: 'Dashboard' },
|
|
137
|
+
{ key: 'nav.translations', fr: 'Traductions', en: 'Translations' },
|
|
138
|
+
{ key: 'nav.unused', fr: 'Inutilisées', en: 'Unused' },
|
|
139
|
+
{ key: 'nav.languages', fr: 'Langues', en: 'Languages' },
|
|
140
|
+
{ key: 'nav.review', fr: 'À réviser', en: 'Review' },
|
|
141
|
+
{ key: 'nav.settings', fr: 'Paramètres', en: 'Settings' },
|
|
142
|
+
{ key: 'nav.users', fr: 'Utilisateurs', en: 'Users' },
|
|
143
|
+
// Sidebar
|
|
144
|
+
{ key: 'sidebar.project_label', fr: 'PROJET', en: 'PROJECT' },
|
|
145
|
+
{ key: 'sidebar.no_project', fr: 'Aucun projet configuré', en: 'No project configured' },
|
|
146
|
+
{ key: 'sidebar.add_project', fr: 'Ajouter un projet', en: 'Add a project' },
|
|
147
|
+
{ key: 'sidebar.manage_projects', fr: 'Gérer les projets', en: 'Manage projects' },
|
|
148
|
+
{ key: 'sidebar.scan', fr: 'Scanner le projet', en: 'Scan project' },
|
|
149
|
+
{ key: 'sidebar.sync', fr: 'Sync JSON', en: 'Sync JSON' },
|
|
150
|
+
{ key: 'sidebar.no_project_selected', fr: 'Sélectionnez un projet pour commencer', en: 'Select a project to get started' },
|
|
151
|
+
{ key: 'sidebar.theme', fr: 'Thème', en: 'Theme' },
|
|
152
|
+
{ key: 'sidebar.ui_lang', fr: 'Langue interface', en: 'UI Language' },
|
|
153
|
+
// Project empty state
|
|
154
|
+
{ key: 'project.none_selected', fr: 'Aucun projet sélectionné', en: 'No project selected' },
|
|
155
|
+
{ key: 'project.none_selected_hint', fr: 'Ajoutez votre projet Vue.js pour commencer à gérer vos traductions.', en: 'Add your Vue.js project to start managing your translations.' },
|
|
156
|
+
{ key: 'project.add_button', fr: 'Ajouter un projet', en: 'Add a project' },
|
|
157
|
+
// User menu
|
|
158
|
+
{ key: 'user.super_admin', fr: 'Super Admin', en: 'Super Admin' },
|
|
159
|
+
{ key: 'user.role_user', fr: 'Utilisateur', en: 'User' },
|
|
160
|
+
{ key: 'user.profile', fr: 'Mon profil', en: 'My profile' },
|
|
161
|
+
{ key: 'user.change_password', fr: 'Changer le mot de passe', en: 'Change password' },
|
|
162
|
+
{ key: 'user.logout', fr: 'Se déconnecter', en: 'Log out' },
|
|
163
|
+
{ key: 'user.change_password_title', fr: 'Changer le mot de passe', en: 'Change password' },
|
|
164
|
+
{ key: 'user.current_password', fr: 'Mot de passe actuel', en: 'Current password' },
|
|
165
|
+
{ key: 'user.new_password', fr: 'Nouveau mot de passe', en: 'New password' },
|
|
166
|
+
{ key: 'user.confirm_password', fr: 'Confirmer', en: 'Confirm' },
|
|
167
|
+
{ key: 'user.password_hint', fr: 'Minimum 8 caractères', en: 'Minimum 8 characters' },
|
|
168
|
+
{ key: 'user.save_password', fr: 'Modifier', en: 'Save' },
|
|
169
|
+
{ key: 'user.password_changed', fr: 'Mot de passe modifié', en: 'Password changed' },
|
|
170
|
+
// Translations page
|
|
171
|
+
{ key: 'translations.title', fr: 'Traductions', en: 'Translations' },
|
|
172
|
+
{ key: 'translations.keys_count', fr: 'clés', en: 'keys' },
|
|
173
|
+
{ key: 'translations.langs_count', fr: 'langues', en: 'languages' },
|
|
174
|
+
{ key: 'translations.search', fr: 'Rechercher une clé...', en: 'Search for a key...' },
|
|
175
|
+
{ key: 'translations.add_key', fr: 'Nouvelle clé', en: 'New key' },
|
|
176
|
+
{ key: 'translations.translate_all', fr: 'Traduire tout', en: 'Translate all' },
|
|
177
|
+
{ key: 'translations.no_results', fr: 'Aucune clé trouvée', en: 'No keys found' },
|
|
178
|
+
{ key: 'translations.no_results_hint', fr: 'Essayez un autre terme de recherche.', en: 'Try a different search term.' },
|
|
179
|
+
{ key: 'translations.add_description', fr: 'Ajouter une description…', en: 'Add a description…' },
|
|
180
|
+
{ key: 'translations.click_to_add', fr: 'Cliquer pour ajouter...', en: 'Click to add...' },
|
|
181
|
+
{ key: 'translations.save', fr: 'Sauvegarder', en: 'Save' },
|
|
182
|
+
{ key: 'translations.cancel', fr: 'Annuler', en: 'Cancel' },
|
|
183
|
+
{ key: 'translations.add_key_title', fr: 'Nouvelle clé de traduction', en: 'New translation key' },
|
|
184
|
+
{ key: 'translations.key_label', fr: 'Clé', en: 'Key' },
|
|
185
|
+
{ key: 'translations.key_hint', fr: 'Exemple: home.title ou nav.menu.about', en: 'Example: home.title or nav.menu.about' },
|
|
186
|
+
{ key: 'translations.description_label', fr: 'Description', en: 'Description' },
|
|
187
|
+
{ key: 'translations.description_hint', fr: 'Contexte pour les traducteurs', en: 'Context for translators' },
|
|
188
|
+
{ key: 'translations.create', fr: 'Créer', en: 'Create' },
|
|
189
|
+
{ key: 'translations.delete_key', fr: 'Supprimer la clé', en: 'Delete key' },
|
|
190
|
+
{ key: 'translations.key_deleted', fr: 'Clé supprimée', en: 'Key deleted' },
|
|
191
|
+
{ key: 'translations.history', fr: 'Historique par langue', en: 'History by language' },
|
|
192
|
+
{ key: 'translations.references', fr: 'référence', en: 'reference' },
|
|
193
|
+
{ key: 'translations.references_plural', fr: 'références', en: 'references' },
|
|
194
|
+
// Status
|
|
195
|
+
{ key: 'status.all', fr: 'Tout', en: 'All' },
|
|
196
|
+
{ key: 'status.draft', fr: 'Brouillon', en: 'Draft' },
|
|
197
|
+
{ key: 'status.reviewed', fr: 'Relu', en: 'Reviewed' },
|
|
198
|
+
{ key: 'status.approved', fr: 'Approuvé', en: 'Approved' },
|
|
199
|
+
{ key: 'status.missing', fr: 'Manquant', en: 'Missing' },
|
|
200
|
+
{ key: 'status.unused', fr: 'Inutilisé', en: 'Unused' },
|
|
201
|
+
{ key: 'status.rejected', fr: 'Refusé', en: 'Rejected' },
|
|
202
|
+
// Key detail page
|
|
203
|
+
{ key: 'key.rejected_notice', fr: 'Cette traduction a été refusée. Merci de la mettre à jour.', en: 'This translation was rejected. Please update it.' },
|
|
204
|
+
{ key: 'key.restore', fr: 'Restaurer', en: 'Restore' },
|
|
205
|
+
// Review page
|
|
206
|
+
{ key: 'review.title', fr: 'File de révision', en: 'Review queue' },
|
|
207
|
+
{ key: 'review.approve_all', fr: 'Tout approuver', en: 'Approve all' },
|
|
208
|
+
{ key: 'review.empty_title', fr: 'Aucune traduction en attente', en: 'No translations pending' },
|
|
209
|
+
{ key: 'review.empty_hint', fr: 'Toutes les traductions relues ont déjà été approuvées.', en: 'All reviewed translations have already been approved.' },
|
|
210
|
+
{ key: 'review.approve', fr: 'Approuver', en: 'Approve' },
|
|
211
|
+
{ key: 'review.back_to_draft', fr: 'Repasser en brouillon', en: 'Back to draft' },
|
|
212
|
+
{ key: 'review.approved_toast', fr: 'Approuvées', en: 'Approved' },
|
|
213
|
+
// Languages page
|
|
214
|
+
{ key: 'languages.title', fr: 'Langues', en: 'Languages' },
|
|
215
|
+
{ key: 'languages.subtitle', fr: 'Gérez les langues du projet', en: 'Manage project languages' },
|
|
216
|
+
{ key: 'languages.add', fr: 'Ajouter une langue', en: 'Add a language' },
|
|
217
|
+
{ key: 'languages.none', fr: 'Aucune langue configurée', en: 'No language configured' },
|
|
218
|
+
{ key: 'languages.none_hint', fr: 'Ajoutez des langues pour commencer à traduire.', en: 'Add languages to start translating.' },
|
|
219
|
+
{ key: 'languages.default_badge', fr: 'Défaut', en: 'Default' },
|
|
220
|
+
// Settings
|
|
221
|
+
{ key: 'settings.title', fr: 'Paramètres', en: 'Settings' },
|
|
222
|
+
{ key: 'settings.save', fr: 'Sauvegarder', en: 'Save' },
|
|
223
|
+
// Users
|
|
224
|
+
{ key: 'users.title', fr: 'Utilisateurs', en: 'Users' },
|
|
225
|
+
{ key: 'users.subtitle', fr: 'Gérez les accès au dashboard', en: 'Manage dashboard access' },
|
|
226
|
+
{ key: 'users.add', fr: 'Ajouter un utilisateur', en: 'Add a user' },
|
|
227
|
+
{ key: 'users.none', fr: 'Aucun utilisateur', en: 'No users' },
|
|
228
|
+
{ key: 'users.never_connected', fr: 'Jamais connecté', en: 'Never logged in' },
|
|
229
|
+
{ key: 'users.role_translator', fr: 'Traducteur', en: 'Translator' },
|
|
230
|
+
{ key: 'users.role_moderator', fr: 'Modérateur', en: 'Moderator' },
|
|
231
|
+
{ key: 'users.role_admin', fr: 'Admin', en: 'Admin' },
|
|
232
|
+
{ key: 'users.inactive', fr: 'Inactif', en: 'Inactive' },
|
|
233
|
+
{ key: 'users.no_role', fr: 'Aucun rôle', en: 'No role' },
|
|
234
|
+
// Login
|
|
235
|
+
{ key: 'login.title', fr: 'Connexion', en: 'Log in' },
|
|
236
|
+
{ key: 'login.email', fr: 'Email', en: 'Email' },
|
|
237
|
+
{ key: 'login.password', fr: 'Mot de passe', en: 'Password' },
|
|
238
|
+
{ key: 'login.submit', fr: 'Se connecter', en: 'Sign in' },
|
|
239
|
+
{ key: 'login.error', fr: 'Identifiants incorrects', en: 'Invalid credentials' },
|
|
240
|
+
// Setup
|
|
241
|
+
{ key: 'setup.title', fr: 'Configuration initiale', en: 'Initial setup' },
|
|
242
|
+
{ key: 'setup.submit', fr: 'Créer le compte administrateur', en: 'Create administrator account' },
|
|
243
|
+
// Dashboard page
|
|
244
|
+
{ key: 'dashboard.title', fr: 'Dashboard', en: 'Dashboard' },
|
|
245
|
+
{ key: 'dashboard.total_keys', fr: 'clés totales', en: 'total keys' },
|
|
246
|
+
{ key: 'dashboard.unused_keys', fr: 'clés inutilisées', en: 'unused keys' },
|
|
247
|
+
{ key: 'dashboard.coverage', fr: 'couverture', en: 'coverage' },
|
|
248
|
+
{ key: 'dashboard.recent_activity', fr: 'Activité récente', en: 'Recent activity' },
|
|
249
|
+
{ key: 'dashboard.no_activity', fr: 'Aucune activité récente', en: 'No recent activity' },
|
|
250
|
+
// Common
|
|
251
|
+
{ key: 'common.cancel', fr: 'Annuler', en: 'Cancel' },
|
|
252
|
+
{ key: 'common.save', fr: 'Sauvegarder', en: 'Save' },
|
|
253
|
+
{ key: 'common.delete', fr: 'Supprimer', en: 'Delete' },
|
|
254
|
+
{ key: 'common.add', fr: 'Ajouter', en: 'Add' },
|
|
255
|
+
{ key: 'common.edit', fr: 'Modifier', en: 'Edit' },
|
|
256
|
+
{ key: 'common.error', fr: 'Erreur', en: 'Error' },
|
|
257
|
+
{ key: 'common.close', fr: 'Fermer', en: 'Close' },
|
|
258
|
+
{ key: 'common.copied', fr: 'Copié !', en: 'Copied!' },
|
|
259
|
+
{ key: 'common.create', fr: 'Créer', en: 'Create' },
|
|
260
|
+
// Onboarding
|
|
261
|
+
{ key: 'onboarding.title', fr: 'Bienvenue sur i18n Dashboard', en: 'Welcome to i18n Dashboard' },
|
|
262
|
+
{ key: 'onboarding.subtitle', fr: 'Configurons votre espace de travail en quelques étapes', en: 'Let\'s set up your workspace in a few steps' },
|
|
263
|
+
{ key: 'onboarding.step_admin', fr: 'Compte admin', en: 'Admin account' },
|
|
264
|
+
{ key: 'onboarding.step_languages', fr: 'Langues interface', en: 'UI Languages' },
|
|
265
|
+
{ key: 'onboarding.step_project', fr: 'Premier projet', en: 'First project' },
|
|
266
|
+
{ key: 'onboarding.step_done', fr: 'Terminé', en: 'Done' },
|
|
267
|
+
{ key: 'onboarding.admin_done', fr: 'Compte administrateur créé avec succès.', en: 'Administrator account created successfully.' },
|
|
268
|
+
{ key: 'onboarding.languages_title', fr: 'Langues de l\'interface', en: 'Interface languages' },
|
|
269
|
+
{ key: 'onboarding.languages_hint', fr: 'Sélectionnez les langues disponibles pour l\'interface du dashboard.', en: 'Select the available languages for the dashboard interface.' },
|
|
270
|
+
{ key: 'onboarding.languages_search', fr: 'Rechercher une langue...', en: 'Search for a language...' },
|
|
271
|
+
{ key: 'onboarding.languages_selected', fr: 'langue(s) sélectionnée(s)', en: 'language(s) selected' },
|
|
272
|
+
{ key: 'onboarding.languages_default', fr: 'Langue par défaut', en: 'Default language' },
|
|
273
|
+
{ key: 'onboarding.project_title', fr: 'Votre premier projet', en: 'Your first project' },
|
|
274
|
+
{ key: 'onboarding.project_hint', fr: 'Configurez le projet Vue.js que vous souhaitez gérer.', en: 'Configure the Vue.js project you want to manage.' },
|
|
275
|
+
{ key: 'onboarding.project_skip', fr: 'Passer cette étape', en: 'Skip this step' },
|
|
276
|
+
{ key: 'onboarding.done_title', fr: 'Tout est prêt !', en: 'All set!' },
|
|
277
|
+
{ key: 'onboarding.done_hint', fr: 'Votre dashboard est configuré. Vous pouvez maintenant gérer vos traductions.', en: 'Your dashboard is configured. You can now manage your translations.' },
|
|
278
|
+
{ key: 'onboarding.go_to_dashboard', fr: 'Aller au dashboard', en: 'Go to dashboard' },
|
|
279
|
+
{ key: 'onboarding.next', fr: 'Suivant', en: 'Next' },
|
|
280
|
+
{ key: 'onboarding.previous', fr: 'Précédent', en: 'Previous' },
|
|
281
|
+
{ key: 'onboarding.finish', fr: 'Terminer', en: 'Finish' },
|
|
282
|
+
// Onboarding — DB step
|
|
283
|
+
{ key: 'onboarding.step_db', fr: 'Base de données', en: 'Database' },
|
|
284
|
+
{ key: 'onboarding.db_title', fr: 'Base de données', en: 'Database' },
|
|
285
|
+
{ key: 'onboarding.db_subtitle', fr: 'Configurez la connexion à votre base de données. Les valeurs sont pré-remplies depuis votre fichier de configuration.', en: 'Configure your database connection. Values are pre-filled from your config file.' },
|
|
286
|
+
{ key: 'onboarding.db_type_label', fr: 'Type de base de données', en: 'Database type' },
|
|
287
|
+
{ key: 'onboarding.db_type_sqlite', fr: 'SQLite (fichier local)', en: 'SQLite (local file)' },
|
|
288
|
+
{ key: 'onboarding.db_type_postgresql', fr: 'PostgreSQL', en: 'PostgreSQL' },
|
|
289
|
+
{ key: 'onboarding.db_type_mysql', fr: 'MySQL / MariaDB', en: 'MySQL / MariaDB' },
|
|
290
|
+
{ key: 'onboarding.db_file_label', fr: 'Fichier de base de données', en: 'Database file' },
|
|
291
|
+
{ key: 'onboarding.db_create_file', fr: 'Créer', en: 'Create' },
|
|
292
|
+
{ key: 'onboarding.db_file_found', fr: 'Fichier trouvé', en: 'File found' },
|
|
293
|
+
{ key: 'onboarding.db_file_missing', fr: 'Le fichier n\'existe pas encore. Cliquez sur « Créer » pour le créer.', en: 'The file does not exist yet. Click "Create" to create it.' },
|
|
294
|
+
{ key: 'onboarding.db_host', fr: 'Hôte', en: 'Host' },
|
|
295
|
+
{ key: 'onboarding.db_port', fr: 'Port', en: 'Port' },
|
|
296
|
+
{ key: 'onboarding.db_user', fr: 'Utilisateur', en: 'User' },
|
|
297
|
+
{ key: 'onboarding.db_password', fr: 'Mot de passe', en: 'Password' },
|
|
298
|
+
{ key: 'onboarding.db_name', fr: 'Base de données', en: 'Database name' },
|
|
299
|
+
{ key: 'onboarding.db_test', fr: 'Tester la connexion', en: 'Test connection' },
|
|
300
|
+
{ key: 'onboarding.db_test_apply', fr: 'Appliquer', en: 'Apply' },
|
|
301
|
+
{ key: 'onboarding.db_connected', fr: 'Connexion OK', en: 'Connection OK' },
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
async function ensureDashboardUIProject(db: Knex): Promise<void> {
|
|
305
|
+
// Check if system project exists
|
|
306
|
+
let systemProject = await db('projects').where({ is_system: true }).first()
|
|
307
|
+
|
|
308
|
+
if (!systemProject) {
|
|
309
|
+
const [id] = await db('projects').insert({
|
|
310
|
+
name: 'Dashboard UI',
|
|
311
|
+
root_path: '__DASHBOARD_UI__',
|
|
312
|
+
locales_path: '',
|
|
313
|
+
key_separator: '.',
|
|
314
|
+
color: 'violet',
|
|
315
|
+
description: 'Textes de l\'interface du dashboard — gérez les traductions de l\'UI ici.',
|
|
316
|
+
is_system: true,
|
|
317
|
+
})
|
|
318
|
+
systemProject = { id }
|
|
319
|
+
console.log('[i18n-dashboard] Projet système "Dashboard UI" créé.')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const projectId = systemProject.id
|
|
323
|
+
|
|
324
|
+
// Load translations from JSON files (prefer file content over hardcoded fallback)
|
|
325
|
+
const frFromFile = readLocaleFile('fr')
|
|
326
|
+
const enFromFile = readLocaleFile('en')
|
|
327
|
+
|
|
328
|
+
// Merge: JSON file takes priority over hardcoded array
|
|
329
|
+
const frMap: Record<string, string> = {}
|
|
330
|
+
const enMap: Record<string, string> = {}
|
|
331
|
+
for (const item of DASHBOARD_UI_STRINGS) {
|
|
332
|
+
frMap[item.key] = item.fr
|
|
333
|
+
enMap[item.key] = item.en
|
|
334
|
+
}
|
|
335
|
+
Object.assign(frMap, frFromFile)
|
|
336
|
+
Object.assign(enMap, enFromFile)
|
|
337
|
+
|
|
338
|
+
// All keys from both sources
|
|
339
|
+
const allKeys = [...new Set([...Object.keys(frMap), ...Object.keys(enMap)])]
|
|
340
|
+
|
|
341
|
+
// Seed EN as the base language (translations for other languages are added via onboarding/auto-translate)
|
|
342
|
+
for (const lang of [{ code: 'en', name: 'English', isDefault: true }, { code: 'fr', name: 'Français', isDefault: false }]) {
|
|
343
|
+
const exists = await db('languages').where({ project_id: projectId, code: lang.code }).first()
|
|
344
|
+
if (!exists) {
|
|
345
|
+
await db('languages').insert({
|
|
346
|
+
project_id: projectId,
|
|
347
|
+
code: lang.code,
|
|
348
|
+
name: lang.name,
|
|
349
|
+
is_default: lang.isDefault,
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Seed strings — idempotent: insert missing keys AND missing translations
|
|
355
|
+
for (const key of allKeys) {
|
|
356
|
+
let existingKey = await db('translation_keys').where({ project_id: projectId, key }).first()
|
|
357
|
+
if (!existingKey) {
|
|
358
|
+
const [keyId] = await db('translation_keys').insert({
|
|
359
|
+
project_id: projectId,
|
|
360
|
+
key,
|
|
361
|
+
description: null,
|
|
362
|
+
is_unused: false,
|
|
363
|
+
})
|
|
364
|
+
existingKey = { id: keyId }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Ensure EN and FR translations exist (approved — these are the source of truth)
|
|
368
|
+
for (const [langCode, value] of [['en', enMap[key]], ['fr', frMap[key]]] as [string, string][]) {
|
|
369
|
+
if (!value) continue
|
|
370
|
+
const existingTr = await db('translations')
|
|
371
|
+
.where({ key_id: existingKey.id, language_code: langCode })
|
|
372
|
+
.first()
|
|
373
|
+
if (!existingTr) {
|
|
374
|
+
await db('translations').insert({ key_id: existingKey.id, language_code: langCode, value, status: 'approved' })
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function initDb(): Promise<void> {
|
|
381
|
+
const db = getDb()
|
|
382
|
+
|
|
383
|
+
// ── projects ──────────────────────────────────────────────────────────
|
|
384
|
+
const hasProjects = await db.schema.hasTable('projects')
|
|
385
|
+
if (!hasProjects) {
|
|
386
|
+
await db.schema.createTable('projects', (table) => {
|
|
387
|
+
table.increments('id').primary()
|
|
388
|
+
table.string('name', 200).notNullable()
|
|
389
|
+
table.text('root_path').notNullable().defaultTo('') // absolute path to project root (empty if remote)
|
|
390
|
+
table.text('source_url').nullable() // remote URL for fetching locale JSON files
|
|
391
|
+
table.string('locales_path', 500).defaultTo('src/locales') // relative to root_path
|
|
392
|
+
table.string('key_separator', 10).defaultTo('.')
|
|
393
|
+
table.string('color', 30).defaultTo('primary') // UI accent color
|
|
394
|
+
table.text('description').nullable()
|
|
395
|
+
table.boolean('is_system').defaultTo(false) // reserved for Dashboard UI project
|
|
396
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── languages ─────────────────────────────────────────────────────────
|
|
401
|
+
const hasLanguages = await db.schema.hasTable('languages')
|
|
402
|
+
if (!hasLanguages) {
|
|
403
|
+
await db.schema.createTable('languages', (table) => {
|
|
404
|
+
table.increments('id').primary()
|
|
405
|
+
table.integer('project_id').notNullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
406
|
+
table.string('code', 35).notNullable() // BCP 47: up to 35 chars (e.g. zh-Hant-TW, sr-Latn-RS)
|
|
407
|
+
table.string('name', 100).notNullable()
|
|
408
|
+
table.boolean('is_default').defaultTo(false)
|
|
409
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
410
|
+
table.unique(['project_id', 'code'])
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── translation_keys ──────────────────────────────────────────────────
|
|
415
|
+
const hasKeys = await db.schema.hasTable('translation_keys')
|
|
416
|
+
if (!hasKeys) {
|
|
417
|
+
await db.schema.createTable('translation_keys', (table) => {
|
|
418
|
+
table.increments('id').primary()
|
|
419
|
+
table.integer('project_id').notNullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
420
|
+
table.string('key', 500).notNullable()
|
|
421
|
+
table.text('description').nullable()
|
|
422
|
+
table.boolean('is_unused').defaultTo(false)
|
|
423
|
+
table.timestamp('last_scanned_at').nullable()
|
|
424
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
425
|
+
table.timestamp('updated_at').defaultTo(db.fn.now())
|
|
426
|
+
table.unique(['project_id', 'key'])
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── translations ──────────────────────────────────────────────────────
|
|
431
|
+
const hasTranslations = await db.schema.hasTable('translations')
|
|
432
|
+
if (!hasTranslations) {
|
|
433
|
+
await db.schema.createTable('translations', (table) => {
|
|
434
|
+
table.increments('id').primary()
|
|
435
|
+
table.integer('key_id').notNullable().references('id').inTable('translation_keys').onDelete('CASCADE')
|
|
436
|
+
table.string('language_code', 10).notNullable()
|
|
437
|
+
table.text('value').nullable()
|
|
438
|
+
table.string('status', 20).defaultTo('draft').nullable()
|
|
439
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
440
|
+
table.timestamp('updated_at').defaultTo(db.fn.now())
|
|
441
|
+
table.unique(['key_id', 'language_code'])
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── translation_history ───────────────────────────────────────────────
|
|
446
|
+
const hasHistory = await db.schema.hasTable('translation_history')
|
|
447
|
+
if (!hasHistory) {
|
|
448
|
+
await db.schema.createTable('translation_history', (table) => {
|
|
449
|
+
table.increments('id').primary()
|
|
450
|
+
table.integer('translation_id').notNullable().references('id').inTable('translations').onDelete('CASCADE')
|
|
451
|
+
table.text('old_value').nullable()
|
|
452
|
+
table.text('new_value').nullable()
|
|
453
|
+
table.string('changed_by', 100).defaultTo('user')
|
|
454
|
+
table.timestamp('changed_at').defaultTo(db.fn.now())
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── key_usages ────────────────────────────────────────────────────────
|
|
459
|
+
const hasKeyUsages = await db.schema.hasTable('key_usages')
|
|
460
|
+
if (!hasKeyUsages) {
|
|
461
|
+
await db.schema.createTable('key_usages', (table) => {
|
|
462
|
+
table.increments('id').primary()
|
|
463
|
+
table.integer('key_id').notNullable().references('id').inTable('translation_keys').onDelete('CASCADE')
|
|
464
|
+
table.string('file_path', 1000).notNullable()
|
|
465
|
+
table.integer('line_number').nullable()
|
|
466
|
+
table.string('detected_function', 50).nullable()
|
|
467
|
+
table.timestamp('scanned_at').defaultTo(db.fn.now())
|
|
468
|
+
table.index(['key_id'])
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── users ──────────────────────────────────────────────────────────────
|
|
473
|
+
const hasUsers = await db.schema.hasTable('users')
|
|
474
|
+
if (!hasUsers) {
|
|
475
|
+
await db.schema.createTable('users', (table) => {
|
|
476
|
+
table.increments('id').primary()
|
|
477
|
+
table.string('email', 255).notNullable().unique()
|
|
478
|
+
table.string('name', 200).notNullable()
|
|
479
|
+
table.string('password_hash', 255).notNullable()
|
|
480
|
+
table.boolean('is_super_admin').defaultTo(false)
|
|
481
|
+
table.boolean('is_active').defaultTo(true)
|
|
482
|
+
table.timestamp('last_login_at').nullable()
|
|
483
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── user_project_roles ─────────────────────────────────────────────────
|
|
488
|
+
// project_id = NULL → role applies to ALL projects (global user)
|
|
489
|
+
const hasUserRoles = await db.schema.hasTable('user_project_roles')
|
|
490
|
+
if (!hasUserRoles) {
|
|
491
|
+
await db.schema.createTable('user_project_roles', (table) => {
|
|
492
|
+
table.increments('id').primary()
|
|
493
|
+
table.integer('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE')
|
|
494
|
+
table.integer('project_id').nullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
495
|
+
table.string('role', 20).notNullable().defaultTo('translator') // admin | moderator | translator
|
|
496
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
497
|
+
table.unique(['user_id', 'project_id'])
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── settings (global) ─────────────────────────────────────────────────
|
|
502
|
+
const hasSettings = await db.schema.hasTable('settings')
|
|
503
|
+
if (!hasSettings) {
|
|
504
|
+
await db.schema.createTable('settings', (table) => {
|
|
505
|
+
table.string('key', 100).primary()
|
|
506
|
+
table.text('value').nullable()
|
|
507
|
+
table.timestamp('updated_at').defaultTo(db.fn.now())
|
|
508
|
+
})
|
|
509
|
+
await db('settings').insert([
|
|
510
|
+
{ key: 'scan_exclude', value: 'node_modules,dist,.nuxt,.output,.git' },
|
|
511
|
+
{ key: 'onboarding_completed', value: 'false' },
|
|
512
|
+
])
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── migrations for existing databases ─────────────────────────────────
|
|
516
|
+
// Add status column if missing (older DBs)
|
|
517
|
+
await addColumnIfMissing(db, 'translations', 'status', (t) =>
|
|
518
|
+
t.string('status', 20).defaultTo('draft').nullable(),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
// ── migrate old schema: add project_id to languages / translation_keys ─
|
|
522
|
+
// If tables exist but don't have project_id (pre-multi-project schema),
|
|
523
|
+
// create a default project and assign all existing rows to it.
|
|
524
|
+
const langHasProject = await db.schema.hasColumn('languages', 'project_id')
|
|
525
|
+
const keysHasProject = await db.schema.hasColumn('translation_keys', 'project_id')
|
|
526
|
+
|
|
527
|
+
if (!langHasProject || !keysHasProject) {
|
|
528
|
+
// Ensure at least one project exists to assign rows to
|
|
529
|
+
let defaultProjectId: number
|
|
530
|
+
const firstProject = await db('projects').orderBy('id', 'asc').first()
|
|
531
|
+
if (firstProject) {
|
|
532
|
+
defaultProjectId = firstProject.id
|
|
533
|
+
} else {
|
|
534
|
+
const runtimeCfg = useRuntimeConfig()
|
|
535
|
+
const root = (runtimeCfg.projectRoot as string || '').trim() || process.cwd()
|
|
536
|
+
const [id] = await db('projects').insert({
|
|
537
|
+
name: basename(root) || 'Projet par défaut',
|
|
538
|
+
root_path: resolve(root),
|
|
539
|
+
locales_path: (runtimeCfg.localesPath as string) || 'src/locales',
|
|
540
|
+
key_separator: '.',
|
|
541
|
+
color: 'primary',
|
|
542
|
+
})
|
|
543
|
+
defaultProjectId = id
|
|
544
|
+
console.log(`[i18n-dashboard] Migration: projet par défaut créé (id=${defaultProjectId})`)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!langHasProject) {
|
|
548
|
+
await db.schema.table('languages', (t) => t.integer('project_id').defaultTo(defaultProjectId))
|
|
549
|
+
await db('languages').whereNull('project_id').update({ project_id: defaultProjectId })
|
|
550
|
+
console.log('[i18n-dashboard] Migration: project_id ajouté à languages')
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!keysHasProject) {
|
|
554
|
+
await db.schema.table('translation_keys', (t) => t.integer('project_id').defaultTo(defaultProjectId))
|
|
555
|
+
await db('translation_keys').whereNull('project_id').update({ project_id: defaultProjectId })
|
|
556
|
+
console.log('[i18n-dashboard] Migration: project_id ajouté à translation_keys')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── migration: is_system column on projects ────────────────────────────
|
|
561
|
+
await addColumnIfMissing(db, 'projects', 'is_system', (t) =>
|
|
562
|
+
t.boolean('is_system').defaultTo(false),
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
// ── migration: source_url column on projects ───────────────────────────
|
|
566
|
+
await addColumnIfMissing(db, 'projects', 'source_url', (t) =>
|
|
567
|
+
t.text('source_url').nullable(),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
// ── migration: fallback_code on languages ─────────────────────────────
|
|
571
|
+
await addColumnIfMissing(db, 'languages', 'fallback_code', (t) =>
|
|
572
|
+
t.string('fallback_code', 35).nullable(),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
// ── migration: enlarge languages.code for BCP 47 (PG / MySQL only) ─────
|
|
576
|
+
// SQLite does not enforce VARCHAR length so no action needed there.
|
|
577
|
+
{
|
|
578
|
+
const client = (db.client as any)?.config?.client ?? ''
|
|
579
|
+
if (client === 'pg' || client === 'postgresql') {
|
|
580
|
+
try {
|
|
581
|
+
await db.raw('ALTER TABLE languages ALTER COLUMN code TYPE varchar(35)')
|
|
582
|
+
} catch { /* already 35 or wider */ }
|
|
583
|
+
} else if (client === 'mysql2' || client === 'mysql') {
|
|
584
|
+
try {
|
|
585
|
+
await db.raw('ALTER TABLE languages MODIFY COLUMN code varchar(35) NOT NULL')
|
|
586
|
+
} catch { /* already done */ }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── auto-create default project from I18N_PROJECT_ROOT env var ────────
|
|
591
|
+
// Only when explicitly passed by the CLI (not the empty default)
|
|
592
|
+
const config = useRuntimeConfig()
|
|
593
|
+
const projectRoot = (config.projectRoot as string || '').trim()
|
|
594
|
+
if (projectRoot) {
|
|
595
|
+
const resolvedRoot = resolve(projectRoot)
|
|
596
|
+
const existing = await db('projects').where({ root_path: resolvedRoot }).first()
|
|
597
|
+
if (!existing) {
|
|
598
|
+
const projectName = basename(resolvedRoot) || 'Mon Projet'
|
|
599
|
+
const localesPath = (config.localesPath as string) || 'src/locales'
|
|
600
|
+
const keySep = (config.public?.keySeparator as string) || '.'
|
|
601
|
+
await db('projects').insert({
|
|
602
|
+
name: projectName,
|
|
603
|
+
root_path: resolvedRoot,
|
|
604
|
+
locales_path: localesPath,
|
|
605
|
+
key_separator: keySep,
|
|
606
|
+
color: 'primary',
|
|
607
|
+
})
|
|
608
|
+
console.log(`[i18n-dashboard] Projet auto-créé : "${projectName}" → ${resolvedRoot}`)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── ensure onboarding_completed row exists (migration for older DBs) ───
|
|
613
|
+
const onboardingRow = await db('settings').where({ key: 'onboarding_completed' }).first()
|
|
614
|
+
if (!onboardingRow) {
|
|
615
|
+
await db('settings').insert({ key: 'onboarding_completed', value: 'false' })
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── ensure Dashboard UI system project exists with seeds ───────────────
|
|
619
|
+
await ensureDashboardUIProject(db)
|
|
620
|
+
|
|
621
|
+
// ── project_number_formats ─────────────────────────────────────────────
|
|
622
|
+
const hasNumberFormats = await db.schema.hasTable('project_number_formats')
|
|
623
|
+
if (!hasNumberFormats) {
|
|
624
|
+
await db.schema.createTable('project_number_formats', (table) => {
|
|
625
|
+
table.increments('id').primary()
|
|
626
|
+
table.integer('project_id').notNullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
627
|
+
table.string('locale', 10).notNullable()
|
|
628
|
+
table.string('name', 100).notNullable()
|
|
629
|
+
table.text('options').notNullable().defaultTo('{}') // JSON Intl.NumberFormat options
|
|
630
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
631
|
+
table.unique(['project_id', 'locale', 'name'])
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── project_datetime_formats ────────────────────────────────────────────
|
|
636
|
+
const hasDatetimeFormats = await db.schema.hasTable('project_datetime_formats')
|
|
637
|
+
if (!hasDatetimeFormats) {
|
|
638
|
+
await db.schema.createTable('project_datetime_formats', (table) => {
|
|
639
|
+
table.increments('id').primary()
|
|
640
|
+
table.integer('project_id').notNullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
641
|
+
table.string('locale', 10).notNullable()
|
|
642
|
+
table.string('name', 100).notNullable()
|
|
643
|
+
table.text('options').notNullable().defaultTo('{}') // JSON Intl.DateTimeFormat options
|
|
644
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
645
|
+
table.unique(['project_id', 'locale', 'name'])
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── project_modifiers ────────────────────────────────────────────────────
|
|
650
|
+
const hasModifiers = await db.schema.hasTable('project_modifiers')
|
|
651
|
+
if (!hasModifiers) {
|
|
652
|
+
await db.schema.createTable('project_modifiers', (table) => {
|
|
653
|
+
table.increments('id').primary()
|
|
654
|
+
table.integer('project_id').notNullable().references('id').inTable('projects').onDelete('CASCADE')
|
|
655
|
+
table.string('name', 100).notNullable()
|
|
656
|
+
table.text('body').notNullable() // JS arrow function string e.g. "(str) => str.toUpperCase()"
|
|
657
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
658
|
+
table.unique(['project_id', 'name'])
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── migration: enable_* columns on projects ──────────────────────────────
|
|
663
|
+
await addColumnIfMissing(db, 'projects', 'enable_number_formats', (t) =>
|
|
664
|
+
t.boolean('enable_number_formats').defaultTo(false),
|
|
665
|
+
)
|
|
666
|
+
await addColumnIfMissing(db, 'projects', 'enable_datetime_formats', (t) =>
|
|
667
|
+
t.boolean('enable_datetime_formats').defaultTo(false),
|
|
668
|
+
)
|
|
669
|
+
await addColumnIfMissing(db, 'projects', 'enable_modifiers', (t) =>
|
|
670
|
+
t.boolean('enable_modifiers').defaultTo(false),
|
|
671
|
+
)
|
|
672
|
+
}
|