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,515 @@
1
+ <template>
2
+ <div class="p-6">
3
+ <div class="mb-6">
4
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.title', 'Settings') }}</h1>
5
+ <p class="text-gray-500 dark:text-gray-400 mt-1">{{ t('settings.subtitle', 'Global dashboard configuration') }}</p>
6
+ </div>
7
+
8
+ <!-- Skeleton -->
9
+ <div v-if="pending" class="max-w-2xl space-y-6">
10
+ <UCard v-for="i in 3" :key="i">
11
+ <template #header>
12
+ <USkeleton class="h-5 w-1/3"/>
13
+ </template>
14
+ <div class="space-y-3">
15
+ <USkeleton class="h-4 w-full"/>
16
+ <USkeleton class="h-9 w-full rounded-lg"/>
17
+ <USkeleton class="h-4 w-2/3"/>
18
+ </div>
19
+ </UCard>
20
+ </div>
21
+
22
+ <div v-else class="max-w-2xl space-y-6">
23
+ <!-- Current project settings (editable) -->
24
+ <UCard v-if="currentProject">
25
+ <template #header>
26
+ <div class="flex items-center gap-2">
27
+ <UIcon class="text-primary-500" name="i-heroicons-folder"/>
28
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.current_project', 'Current project') }}</h2>
29
+ </div>
30
+ </template>
31
+ <div class="space-y-4">
32
+ <div class="grid grid-cols-2 gap-4">
33
+ <UFormField class="col-span-2" :label="t('settings.project_name', 'Project name')">
34
+ <UInput v-model="form.name" class="w-full" :placeholder="t('settings.project_name_placeholder', 'My project')"/>
35
+ </UFormField>
36
+ <UFormField :label="t('settings.root_path', 'Root path')" :hint="t('settings.root_path_hint', 'Absolute path to the project root')">
37
+ <PathPicker v-model="form.root_path" class="w-full" />
38
+ </UFormField>
39
+ <UFormField :label="t('settings.source_url', 'Source URL')" :hint="t('settings.source_url_hint', 'App URL (for CORS auto-detection)')">
40
+ <UInput v-model="form.source_url" class="w-full" placeholder="https://my-app.com"/>
41
+ </UFormField>
42
+ <UFormField :label="t('settings.locales_folder', 'Locales folder')" :hint="t('settings.locales_folder_hint', 'Relative to root')">
43
+ <UInput v-model="form.locales_path" class="w-full" placeholder="src/locales"/>
44
+ </UFormField>
45
+ <UFormField :label="t('settings.key_separator', 'Key separator')">
46
+ <UInput v-model="form.key_separator" class="w-full" placeholder="." style="font-family: monospace"/>
47
+ </UFormField>
48
+ <UFormField class="col-span-2" :label="t('settings.description', 'Description')">
49
+ <UTextarea v-model="form.description" class="w-full" :rows="2" :placeholder="t('settings.description_placeholder', 'Project description…')"/>
50
+ </UFormField>
51
+ <UFormField :label="t('settings.color', 'Color')">
52
+ <div class="flex items-center gap-2">
53
+ <input v-model="form.color" type="color" class="h-9 w-12 rounded border border-gray-200 dark:border-gray-700 cursor-pointer p-0.5"/>
54
+ <code class="text-xs font-mono text-gray-500">{{ form.color }}</code>
55
+ </div>
56
+ </UFormField>
57
+ </div>
58
+ </div>
59
+ </UCard>
60
+
61
+ <!-- Scanner settings -->
62
+ <UCard>
63
+ <template #header>
64
+ <div class="flex items-center gap-2">
65
+ <UIcon class="text-blue-500" name="i-heroicons-magnifying-glass"/>
66
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.scanner_title', 'vue-i18n Scanner') }}</h2>
67
+ </div>
68
+ </template>
69
+
70
+ <div class="space-y-4">
71
+ <div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
72
+ <p class="text-sm text-blue-700 dark:text-blue-300">
73
+ <UIcon class="inline mr-1" name="i-heroicons-information-circle"/>
74
+ {{ t('settings.scanner_info', 'The scanner automatically detects all keys used in your code via') }}
75
+ <code class="font-mono text-xs">$t()</code>, <code class="font-mono text-xs">t()</code>,
76
+ <code class="font-mono text-xs">&lt;i18n-t&gt;</code>, <code class="font-mono text-xs">v-t</code>
77
+ {{ t('settings.scanner_info_and', 'and') }} <code class="font-mono text-xs">&lt;i18n&gt;</code> {{ t('settings.scanner_info_blocks', 'blocks in your SFCs.') }}
78
+ </p>
79
+ </div>
80
+
81
+ <UFormField
82
+ :hint="t('settings.scan_exclude_hint', 'Comma-separated')"
83
+ :label="t('settings.scan_exclude', 'Folders excluded from scan')"
84
+ >
85
+ <UInput
86
+ v-model="form.scan_exclude"
87
+ class="w-full"
88
+ placeholder="node_modules,dist,.nuxt,.output"
89
+ />
90
+ </UFormField>
91
+
92
+ <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
93
+ <p class="text-xs text-gray-500 dark:text-gray-400 font-medium mb-2">{{ t('settings.detected_functions', 'Auto-detected functions') }} :</p>
94
+ <div class="flex flex-wrap gap-1.5">
95
+ <code
96
+ v-for="fn in detectedFunctions"
97
+ :key="fn"
98
+ class="text-xs bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded px-1.5 py-0.5 text-gray-700 dark:text-gray-300"
99
+ >{{ fn }}</code>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </UCard>
104
+
105
+ <!-- Format pages -->
106
+ <UCard>
107
+ <template #header>
108
+ <div class="flex items-center gap-2">
109
+ <UIcon class="text-purple-500" name="i-heroicons-adjustments-horizontal"/>
110
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.advanced_title', 'Advanced features') }}</h2>
111
+ </div>
112
+ </template>
113
+ <div class="space-y-4">
114
+ <p class="text-sm text-gray-500 dark:text-gray-400">
115
+ {{ t('settings.advanced_hint', 'Enable advanced configuration pages for this project. They will appear in the sidebar navigation.') }}
116
+ </p>
117
+ <div class="space-y-3">
118
+ <div class="flex items-center justify-between py-1">
119
+ <div>
120
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.number_formats', 'Number formats') }}</p>
121
+ <p class="text-xs text-gray-400">{{ t('settings.number_formats_hint', 'Configure') }} <code class="font-mono">$n(value, 'currency')</code> — Intl.NumberFormat</p>
122
+ </div>
123
+ <UToggle v-model="form.enable_number_formats" />
124
+ </div>
125
+ <div class="flex items-center justify-between py-1 border-t border-gray-100 dark:border-gray-800">
126
+ <div>
127
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.datetime_formats', 'Date formats') }}</p>
128
+ <p class="text-xs text-gray-400">{{ t('settings.datetime_formats_hint', 'Configure') }} <code class="font-mono">$d(date, 'short')</code> — Intl.DateTimeFormat</p>
129
+ </div>
130
+ <UToggle v-model="form.enable_datetime_formats" />
131
+ </div>
132
+ <div class="flex items-center justify-between py-1 border-t border-gray-100 dark:border-gray-800">
133
+ <div>
134
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.custom_modifiers', 'Custom modifiers') }}</p>
135
+ <p class="text-xs text-gray-400">{{ t('settings.custom_modifiers_hint', 'Add custom') }} <code class="font-mono">@.modifier:key</code> {{ t('settings.custom_modifiers_hint2', 'modifiers') }}</p>
136
+ </div>
137
+ <UToggle v-model="form.enable_modifiers" />
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </UCard>
142
+
143
+ <!-- Google Translate settings -->
144
+ <UCard>
145
+ <template #header>
146
+ <div class="flex items-center gap-2">
147
+ <UIcon class="text-yellow-500" name="i-heroicons-sparkles"/>
148
+ <h2 class="font-semibold text-gray-900 dark:text-white">Google Translate</h2>
149
+ </div>
150
+ </template>
151
+
152
+ <div class="space-y-4">
153
+ <div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
154
+ <p class="text-sm text-blue-700 dark:text-blue-300">
155
+ <UIcon class="inline mr-1" name="i-heroicons-information-circle"/>
156
+ {{ t('settings.google_translate_info', 'By default, the free Google Translate API is used (no key required). For production use, add an official key for better rate limits.') }}
157
+ </p>
158
+ </div>
159
+
160
+ <UFormField
161
+ :hint="t('settings.google_api_key_hint', 'Optional. Leave blank to use the free tier.')"
162
+ :label="t('settings.google_api_key', 'Google Translate API Key')"
163
+ >
164
+ <UInput
165
+ v-model="form.google_translate_api_key"
166
+ class="w-full"
167
+ placeholder="AIza..."
168
+ type="password"
169
+ />
170
+ </UFormField>
171
+ </div>
172
+ </UCard>
173
+
174
+ <!-- API Info -->
175
+ <UCard>
176
+ <template #header>
177
+ <div class="flex items-center gap-2">
178
+ <UIcon class="text-green-500" name="i-heroicons-code-bracket"/>
179
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.api_endpoints', 'API Endpoints') }}</h2>
180
+ </div>
181
+ </template>
182
+
183
+ <div class="space-y-3">
184
+ <p class="text-sm text-gray-500 dark:text-gray-400">
185
+ {{ t('settings.api_endpoints_hint', 'Use these endpoints in your vue-i18n configuration:') }}
186
+ </p>
187
+
188
+ <div v-for="example in apiExamples" :key="example.label" class="group">
189
+ <p class="text-xs text-gray-400 mb-1">{{ example.label }}</p>
190
+ <div class="flex items-center gap-2 bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2">
191
+ <code class="text-sm font-mono text-gray-700 dark:text-gray-300 flex-1">{{ example.url }}</code>
192
+ <UButton
193
+ color="neutral"
194
+ icon="i-heroicons-clipboard"
195
+ size="xs"
196
+ variant="ghost"
197
+ @click="copyToClipboard(example.url)"
198
+ />
199
+ </div>
200
+ </div>
201
+
202
+ <div class="mt-4 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
203
+ <p class="text-xs text-gray-400 mb-2 font-mono font-semibold">{{ t('settings.vue_i18n_example', 'vue-i18n configuration example:') }}</p>
204
+ <pre class="text-xs font-mono text-gray-700 dark:text-gray-300 overflow-auto">{{ vueI18nExample }}</pre>
205
+ </div>
206
+ </div>
207
+ </UCard>
208
+
209
+ <!-- Export -->
210
+ <UCard v-if="currentProject">
211
+ <template #header>
212
+ <div class="flex items-center gap-2">
213
+ <UIcon class="text-purple-500" name="i-heroicons-arrow-down-tray"/>
214
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.export_title', 'Export translations') }}</h2>
215
+ </div>
216
+ </template>
217
+
218
+ <div class="space-y-4">
219
+ <!-- All languages -->
220
+ <div class="flex items-center justify-between">
221
+ <div>
222
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.export_all_languages', 'All languages') }}</p>
223
+ <p class="text-xs text-gray-400">{{ t('settings.export_all_languages_hint', 'A single JSON file with all languages') }}</p>
224
+ </div>
225
+ <UButton
226
+ color="neutral"
227
+ icon="i-heroicons-arrow-down-tray"
228
+ size="sm"
229
+ variant="outline"
230
+ @click="downloadAll"
231
+ >
232
+ {{ t('settings.export_all', 'Export all') }}
233
+ </UButton>
234
+ </div>
235
+
236
+ <!-- Per language -->
237
+ <div v-if="projectLanguages.length" class="border-t border-gray-100 dark:border-gray-800 pt-4 space-y-2">
238
+ <p class="text-xs text-gray-400 font-medium uppercase tracking-wide mb-3">{{ t('settings.export_per_language', 'Per language') }}</p>
239
+ <div
240
+ v-for="lang in projectLanguages"
241
+ :key="lang.code"
242
+ class="flex items-center justify-between py-1.5"
243
+ >
244
+ <div class="flex items-center gap-2">
245
+ <span class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ lang.name }}</span>
246
+ <code class="text-xs text-gray-400 font-mono">{{ lang.code }}</code>
247
+ <UBadge v-if="lang.is_default" color="primary" size="xs" variant="soft">{{ t('languages.default_badge', 'Default') }}</UBadge>
248
+ </div>
249
+ <UButton
250
+ color="neutral"
251
+ icon="i-heroicons-arrow-down-tray"
252
+ size="xs"
253
+ variant="ghost"
254
+ @click="downloadLang(lang.code)"
255
+ >
256
+ {{ lang.code }}.json
257
+ </UButton>
258
+ </div>
259
+ </div>
260
+
261
+ <p v-else class="text-xs text-gray-400 italic">{{ t('settings.no_languages', 'No language configured for this project.') }}</p>
262
+ </div>
263
+ </UCard>
264
+
265
+ <!-- Snapshot export / import -->
266
+ <UCard v-if="currentProject">
267
+ <template #header>
268
+ <div class="flex items-center gap-2">
269
+ <UIcon class="text-indigo-500" name="i-heroicons-archive-box"/>
270
+ <h2 class="font-semibold text-gray-900 dark:text-white">{{ t('settings.snapshot_title', 'Project snapshot') }}</h2>
271
+ </div>
272
+ </template>
273
+ <div class="space-y-5">
274
+ <p class="text-sm text-gray-500 dark:text-gray-400">
275
+ {{ t('settings.snapshot_hint', 'Export a complete snapshot of this project (config, languages, all translation keys and values) as a single JSON file. Import it on any other instance to restore.') }}
276
+ </p>
277
+
278
+ <!-- Export -->
279
+ <div class="flex items-center justify-between">
280
+ <div>
281
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.snapshot_export', 'Export snapshot') }}</p>
282
+ <p class="text-xs text-gray-400">{{ t('settings.snapshot_export_hint', 'Full backup: config + languages + all keys') }}</p>
283
+ </div>
284
+ <UButton
285
+ color="indigo"
286
+ icon="i-heroicons-arrow-down-tray"
287
+ size="sm"
288
+ variant="outline"
289
+ @click="downloadSnapshot"
290
+ >
291
+ {{ t('settings.snapshot_export_btn', 'Export') }}
292
+ </UButton>
293
+ </div>
294
+
295
+ <!-- Import -->
296
+ <div class="border-t border-gray-100 dark:border-gray-800 pt-4 space-y-3">
297
+ <div class="flex items-start justify-between gap-4">
298
+ <div>
299
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200">{{ t('settings.snapshot_import', 'Import snapshot') }}</p>
300
+ <p class="text-xs text-gray-400">{{ t('settings.snapshot_import_hint', 'Merge or replace the current project with an exported snapshot') }}</p>
301
+ </div>
302
+ <div class="flex items-center gap-2 shrink-0">
303
+ <USelect
304
+ v-model="importMode"
305
+ :items="importModes"
306
+ size="xs"
307
+ class="w-28"
308
+ />
309
+ <UButton
310
+ color="indigo"
311
+ icon="i-heroicons-arrow-up-tray"
312
+ size="sm"
313
+ variant="outline"
314
+ :loading="importing"
315
+ @click="triggerImport"
316
+ >
317
+ {{ t('settings.snapshot_import_btn', 'Import') }}
318
+ </UButton>
319
+ </div>
320
+ </div>
321
+ <input
322
+ ref="fileInput"
323
+ type="file"
324
+ accept=".json"
325
+ class="hidden"
326
+ @change="onFileSelected"
327
+ />
328
+ <div v-if="importMode === 'replace'" class="flex items-start gap-2 bg-amber-50 dark:bg-amber-900/20 rounded-lg px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
329
+ <UIcon name="i-heroicons-exclamation-triangle" class="shrink-0 mt-0.5"/>
330
+ {{ t('settings.snapshot_replace_warning', 'Replace mode: all existing keys and translations will be deleted before import.') }}
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </UCard>
335
+
336
+ <!-- Save button -->
337
+ <div class="flex justify-end">
338
+ <UButton :loading="saving" icon="i-heroicons-check" @click="onSave">
339
+ {{ t('settings.save', 'Save') }}
340
+ </UButton>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </template>
345
+
346
+ <script lang="ts" setup>
347
+ const toast = useToast()
348
+ const { t } = useT()
349
+ const { currentProject, fetchProjects } = useProject()
350
+ const { settings, pending, saving, saveSettings } = useSettings()
351
+ const { projectLanguages } = useLanguages()
352
+
353
+ const detectedFunctions = [
354
+ '$t()', '$tc()', '$te()', '$tm()',
355
+ 't() via useI18n', 'tc()', 'te()', 'tm()',
356
+ 'i18n.t()', 'i18n.global.t()',
357
+ '<i18n-t keypath="...">',
358
+ 'v-t="\'key\'"',
359
+ 'Bloc <i18n> SFC',
360
+ ]
361
+
362
+ const form = ref({
363
+ scan_exclude: 'node_modules,dist,.nuxt,.output',
364
+ google_translate_api_key: '',
365
+ enable_number_formats: false,
366
+ enable_datetime_formats: false,
367
+ enable_modifiers: false,
368
+ // Project fields
369
+ name: '',
370
+ root_path: '',
371
+ source_url: '',
372
+ locales_path: 'src/locales',
373
+ key_separator: '.',
374
+ color: '#6366f1',
375
+ description: '',
376
+ })
377
+
378
+ watch(settings, (val) => {
379
+ if (val) {
380
+ form.value.scan_exclude = val.scan_exclude || 'node_modules,dist,.nuxt,.output'
381
+ form.value.google_translate_api_key = val.google_translate_api_key || ''
382
+ }
383
+ }, { immediate: true })
384
+
385
+ watch(currentProject, (val) => {
386
+ if (val) {
387
+ form.value.enable_number_formats = val.enable_number_formats || false
388
+ form.value.enable_datetime_formats = val.enable_datetime_formats || false
389
+ form.value.enable_modifiers = val.enable_modifiers || false
390
+ form.value.name = val.name || ''
391
+ form.value.root_path = val.root_path || ''
392
+ form.value.source_url = val.source_url || ''
393
+ form.value.locales_path = val.locales_path || 'src/locales'
394
+ form.value.key_separator = val.key_separator || '.'
395
+ form.value.color = val.color || '#6366f1'
396
+ form.value.description = val.description || ''
397
+ }
398
+ }, { immediate: true })
399
+
400
+ const apiAddress = document.location.origin
401
+
402
+ const apiExamples = computed(() => [
403
+ { label: t('settings.api_example_english', 'English translations'), url: `${apiAddress}/locale/en.json` },
404
+ { label: t('settings.api_example_generic', 'Generic pattern'), url: `[i18n_dahsboard_address]/locale/[lang].json` },
405
+ ])
406
+
407
+ const vueI18nExample = computed(() => `import { createI18n } from 'vue-i18n'
408
+
409
+ const i18n = createI18n({
410
+ locale: 'en',
411
+ messages: {
412
+ en: await fetch('${apiAddress}/locale/en.json').then(r => r.json()),
413
+ [lang]: await fetch('[i18n_dahsboard_address]/locale/[lang].json').then(r => r.json()),
414
+ }
415
+ })`)
416
+
417
+ async function copyToClipboard (text: string) {
418
+ await navigator.clipboard.writeText(text)
419
+ toast.add({ title: t('common.copied', 'Copied!'), color: 'success' })
420
+ }
421
+
422
+ async function onSave () {
423
+ await saveSettings({ scan_exclude: form.value.scan_exclude, google_translate_api_key: form.value.google_translate_api_key })
424
+ if (currentProject.value) {
425
+ await $fetch(`/api/projects/${currentProject.value.id}`, {
426
+ method: 'PUT',
427
+ body: {
428
+ name: form.value.name,
429
+ root_path: form.value.root_path,
430
+ source_url: form.value.source_url,
431
+ locales_path: form.value.locales_path,
432
+ key_separator: form.value.key_separator,
433
+ color: form.value.color,
434
+ description: form.value.description,
435
+ enable_number_formats: form.value.enable_number_formats,
436
+ enable_datetime_formats: form.value.enable_datetime_formats,
437
+ enable_modifiers: form.value.enable_modifiers,
438
+ },
439
+ })
440
+ await fetchProjects()
441
+ }
442
+ }
443
+
444
+ function triggerDownload(url: string, filename: string) {
445
+ const a = document.createElement('a')
446
+ a.href = url
447
+ a.download = filename
448
+ a.click()
449
+ }
450
+
451
+ function downloadAll() {
452
+ if (!currentProject.value) return
453
+ const projectName = currentProject.value.name.replace(/[^a-z0-9]/gi, '_')
454
+ triggerDownload(`/api/export?project_id=${currentProject.value.id}`, `${projectName}_all.json`)
455
+ }
456
+
457
+ function downloadLang(code: string) {
458
+ if (!currentProject.value) return
459
+ const projectName = currentProject.value.name.replace(/[^a-z0-9]/gi, '_')
460
+ triggerDownload(`/api/export?project_id=${currentProject.value.id}&lang=${code}`, `${projectName}_${code}.json`)
461
+ }
462
+
463
+ // ── Snapshot ───────────────────────────────────────────────────────────────
464
+
465
+ function downloadSnapshot() {
466
+ if (!currentProject.value) return
467
+ const projectName = currentProject.value.name.replace(/[^a-z0-9]/gi, '_')
468
+ triggerDownload(`/api/project-snapshot?project_id=${currentProject.value.id}`, `${projectName}_snapshot.json`)
469
+ }
470
+
471
+ const importMode = ref<'merge' | 'replace'>('merge')
472
+ const importModes = computed(() => [
473
+ { label: t('settings.snapshot_mode_merge', 'Merge'), value: 'merge' },
474
+ { label: t('settings.snapshot_mode_replace', 'Replace'), value: 'replace' },
475
+ ])
476
+ const importing = ref(false)
477
+ const fileInput = ref<HTMLInputElement>()
478
+
479
+ function triggerImport() {
480
+ fileInput.value?.click()
481
+ }
482
+
483
+ async function onFileSelected(event: Event) {
484
+ const file = (event.target as HTMLInputElement).files?.[0]
485
+ if (!file || !currentProject.value) return
486
+ ;(event.target as HTMLInputElement).value = ''
487
+
488
+ let snapshot: any
489
+ try {
490
+ snapshot = JSON.parse(await file.text())
491
+ } catch {
492
+ toast.add({ title: t('settings.snapshot_parse_error', 'Invalid JSON file'), color: 'error' })
493
+ return
494
+ }
495
+
496
+ importing.value = true
497
+ try {
498
+ const result = await $fetch('/api/project-snapshot', {
499
+ method: 'POST',
500
+ body: { snapshot, project_id: currentProject.value.id, mode: importMode.value },
501
+ }) as any
502
+
503
+ await fetchProjects()
504
+ toast.add({
505
+ title: t('settings.snapshot_import_success', 'Snapshot imported'),
506
+ description: `+${result.stats.keys_added} keys · ${result.stats.translations_added} translations added`,
507
+ color: 'success',
508
+ })
509
+ } catch (e: any) {
510
+ toast.add({ title: t('settings.snapshot_import_error', 'Import failed'), description: e?.data?.message, color: 'error' })
511
+ } finally {
512
+ importing.value = false
513
+ }
514
+ }
515
+ </script>