i18n-dashboard 0.4.3 → 0.6.2
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/README.md +16 -11
- package/assets/locales/en.json +80 -23
- package/components/GitRepoManager.vue +41 -0
- package/components/LanguagePicker.vue +145 -0
- package/components/ScanModal.vue +38 -25
- package/components/TranslationRow.vue +14 -3
- package/composables/useKeys.ts +12 -11
- package/composables/useLanguages.ts +8 -5
- package/composables/useProfile.ts +10 -8
- package/composables/useProject.ts +16 -12
- package/composables/useReview.ts +3 -2
- package/composables/useSettings.ts +3 -2
- package/composables/useUsers.ts +6 -5
- package/consts/commons.const.js +6 -0
- package/interfaces/project.interface.ts +13 -7
- package/layouts/default.vue +24 -3
- package/package.json +1 -1
- package/pages/projects/[id]/settings.vue +47 -21
- package/pages/projects/index.vue +316 -101
- package/pages/users/[id]/profile.vue +35 -23
- package/server/api/profile.get.ts +20 -8
- package/server/api/projects/[id].put.ts +11 -5
- package/server/api/projects/check-name.get.ts +16 -0
- package/server/api/projects/detect.post.ts +113 -0
- package/server/api/projects/index.get.ts +1 -0
- package/server/api/projects/index.post.ts +14 -7
- package/server/api/scan.post.ts +77 -78
- package/server/api/sync.post.ts +110 -139
- package/server/api/translations/index.post.ts +1 -1
- package/server/api/users/[id]/profile.get.ts +21 -8
- package/server/db/index.ts +6 -0
- package/server/interfaces/profile.interface.ts +4 -2
- package/server/utils/scanner.uti.ts +56 -3
- package/services/profile.service.ts +5 -5
package/README.md
CHANGED
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
|
|
31
31
|
### Scan & Sync
|
|
32
32
|
- **Source scan (local)** — browse your file system with the built-in folder picker, detect all `$t()`, `t()`, `<i18n-t>`, `v-t`, and `<i18n>` block usages across `.vue`, `.ts`, `.js` files
|
|
33
|
-
- **Source scan (
|
|
34
|
-
- **
|
|
35
|
-
- **Sync** —
|
|
33
|
+
- **Source scan (git)** — provide a git repository URL (+ optional branch and access token); the dashboard clones the repo, scans source files, and imports locale files in one step
|
|
34
|
+
- **Sync (local)** — import existing `.json` locale files from a local path into the database
|
|
35
|
+
- **Sync (git)** — clone the configured git repository and import locale JSON files; only fills missing or empty translations — never overwrites existing values
|
|
36
36
|
- **Unused key detection** — keys not found in source files are automatically flagged
|
|
37
|
+
- **Conflict-safe import** — sync and scan never overwrite a translation that already has a value in the database
|
|
37
38
|
|
|
38
39
|
### Advanced Formats
|
|
39
40
|
*(enable per project in Settings)*
|
|
@@ -43,7 +44,11 @@
|
|
|
43
44
|
- **Snippet generator** — generates a ready-to-paste `createI18n()` configuration block
|
|
44
45
|
|
|
45
46
|
### Projects
|
|
46
|
-
- **
|
|
47
|
+
- **3-step creation wizard** — Source → Info → Languages: add a local path or git repo, auto-detect project name, locales folder and existing languages, then pick languages with the built-in BCP 47 picker
|
|
48
|
+
- **Auto-detect on creation** — reads `package.json` for the project name and scans the locale folder to pre-fill languages; works with both local paths and git repos
|
|
49
|
+
- **Auto-scan on creation** — a scan is automatically triggered after project creation so keys are immediately available
|
|
50
|
+
- **Git repository per project** — configure a git URL, branch, and optional access token; used for scan and sync operations without requiring a local clone
|
|
51
|
+
- **Inline settings** — edit project name, root path, git repo, locales folder, key separator, color, and description directly from the project settings page
|
|
47
52
|
- **Folder browser** — navigate your file system visually to pick the project root path
|
|
48
53
|
- **Project snapshot** — export a complete backup (config + languages + all keys + translations) as a single JSON file, import it on any other instance (merge or replace mode)
|
|
49
54
|
|
|
@@ -306,7 +311,7 @@ The `{count}` and `{n}` parameters are always available implicitly.
|
|
|
306
311
|
|
|
307
312
|
### Scan
|
|
308
313
|
|
|
309
|
-
Click **Scan project** in the sidebar to open the scan modal.
|
|
314
|
+
Click **Scan project** in the sidebar or on any project card to open the scan modal.
|
|
310
315
|
|
|
311
316
|
**Local mode** — browse your file system with the folder picker and scan `.vue`, `.ts`, `.js` files for:
|
|
312
317
|
- `$t('key')`, `$tc()`, `$te()`, `$tm()`
|
|
@@ -315,13 +320,11 @@ Click **Scan project** in the sidebar to open the scan modal. Three modes are av
|
|
|
315
320
|
- `v-t="'key'"`
|
|
316
321
|
- `<i18n>` SFC blocks
|
|
317
322
|
|
|
318
|
-
**Git mode** —
|
|
323
|
+
**Git mode** — enter a git repository URL (and optionally a branch and access token); the scanner clones the repo (shallow, depth 1), scans all source files, and also imports locale JSON files to populate missing translations in one step. Useful for CI environments or when you don't have a local checkout.
|
|
319
324
|
|
|
320
325
|
> Required token permission: **Contents → Read** (GitHub fine-grained PAT) or **repo** scope (classic PAT).
|
|
321
326
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
Results (keys found, new keys, unused keys, files scanned) are displayed inline.
|
|
327
|
+
Results displayed inline: keys found, new keys, unused keys, files scanned, languages added, translations imported.
|
|
325
328
|
|
|
326
329
|
### Settings
|
|
327
330
|
|
|
@@ -577,8 +580,10 @@ POST /api/translations/batch-translate # Auto-translate all missing for a lang
|
|
|
577
580
|
### Scan & Sync
|
|
578
581
|
|
|
579
582
|
```http
|
|
580
|
-
POST /api/scan
|
|
581
|
-
POST /api/sync
|
|
583
|
+
POST /api/scan # body: { project_id, mode: 'local'|'git', root_path?, git_url?, git_branch?, git_token? }
|
|
584
|
+
POST /api/sync # body: { project_id } — uses project's configured git repo or local path
|
|
585
|
+
POST /api/projects/detect # body: { root_path?, git_url?, git_branch?, git_token? }
|
|
586
|
+
GET /api/projects/check-name?name=&exclude_id= # Availability check
|
|
582
587
|
```
|
|
583
588
|
|
|
584
589
|
### Project Snapshot
|
package/assets/locales/en.json
CHANGED
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
"user.save_password": "Save",
|
|
40
40
|
"user.password_changed": "Password changed",
|
|
41
41
|
"user.passwords_mismatch": "Passwords do not match",
|
|
42
|
+
"keys.created": "Key created",
|
|
43
|
+
"keys.deleted": "Key deleted",
|
|
44
|
+
"keys.description_updated": "Description updated",
|
|
45
|
+
"keys.version_restored": "Version restored",
|
|
46
|
+
"keys.translate_done": "Auto-translate complete",
|
|
47
|
+
"keys.translated": "translated",
|
|
48
|
+
"keys.skipped": "skipped",
|
|
42
49
|
"translations.title": "Translations",
|
|
43
50
|
"translations.keys_count": "keys",
|
|
44
51
|
"translations.langs_count": "languages",
|
|
@@ -86,6 +93,8 @@
|
|
|
86
93
|
"status.rejected": "Rejected",
|
|
87
94
|
"status.missing": "Missing",
|
|
88
95
|
"status.unused": "Unused",
|
|
96
|
+
"review.marked_reviewed": "Marked as reviewed",
|
|
97
|
+
"review.translations_reviewed": "translation(s) marked as reviewed",
|
|
89
98
|
"review.title": "Review queue",
|
|
90
99
|
"review.approve_all": "Approve all",
|
|
91
100
|
"review.empty_title": "No translations pending",
|
|
@@ -99,12 +108,17 @@
|
|
|
99
108
|
"review.mark_all_reviewed": "Mark all as reviewed",
|
|
100
109
|
"review.reject": "Reject",
|
|
101
110
|
"review.mark_reviewed": "Mark as reviewed",
|
|
111
|
+
"languages.deleted": "Language deleted",
|
|
112
|
+
"languages.translating": "Translating",
|
|
113
|
+
"languages.translate_done": "Translation complete",
|
|
114
|
+
"languages.with_errors": "with errors",
|
|
102
115
|
"languages.title": "Languages",
|
|
103
116
|
"languages.subtitle": "Manage project languages",
|
|
104
117
|
"languages.add": "Add a language",
|
|
105
118
|
"languages.none": "No language configured",
|
|
106
119
|
"languages.none_hint": "Add languages to start translating.",
|
|
107
120
|
"languages.default_badge": "Default",
|
|
121
|
+
"settings.saved": "Settings saved",
|
|
108
122
|
"settings.title": "Settings",
|
|
109
123
|
"settings.save": "Save",
|
|
110
124
|
"settings.root_path": "Root path",
|
|
@@ -167,6 +181,12 @@
|
|
|
167
181
|
"formats.number_title": "Number formats",
|
|
168
182
|
"formats.datetime_title": "Date & time formats",
|
|
169
183
|
"formats.modifiers_title": "Modifiers",
|
|
184
|
+
"users.created": "User created",
|
|
185
|
+
"users.invitation_sent": "Invitation sent to",
|
|
186
|
+
"users.access_updated": "Access updated",
|
|
187
|
+
"users.deactivated": "User deactivated",
|
|
188
|
+
"users.reactivated": "User reactivated",
|
|
189
|
+
"users.deleted": "User deleted",
|
|
170
190
|
"users.title": "Users",
|
|
171
191
|
"users.subtitle": "Manage dashboard access",
|
|
172
192
|
"users.all_title": "All users",
|
|
@@ -207,6 +227,9 @@
|
|
|
207
227
|
"users.delete_warning": "This action is irreversible. The account and all associated data will be permanently deleted.",
|
|
208
228
|
"users.global_access_all": "All projects",
|
|
209
229
|
"users.no_access": "No access",
|
|
230
|
+
"projects.created": "Project added",
|
|
231
|
+
"projects.updated": "Project updated",
|
|
232
|
+
"projects.deleted": "Project deleted",
|
|
210
233
|
"projects.subtitle": "Manage your translation projects",
|
|
211
234
|
"projects.add": "Add a project",
|
|
212
235
|
"projects.none": "No project",
|
|
@@ -214,7 +237,11 @@
|
|
|
214
237
|
"projects.add_first": "Add my first project",
|
|
215
238
|
"projects.keys_stat": "keys",
|
|
216
239
|
"projects.separator": "separator",
|
|
217
|
-
"projects.
|
|
240
|
+
"projects.git_repos_count": "git repo(s)",
|
|
241
|
+
"projects.more_repos": "more repo(s)",
|
|
242
|
+
"projects.no_description": "No description",
|
|
243
|
+
"projects.name_taken": "This name is already taken",
|
|
244
|
+
"projects.scan_requires_local": "Requires a local path or git repo",
|
|
218
245
|
"projects.scan_tooltip": "Scan source files to detect translation keys",
|
|
219
246
|
"projects.sync_requires_source": "Requires a local path or remote URL",
|
|
220
247
|
"projects.sync_tooltip": "Import locale JSON files into the database",
|
|
@@ -249,6 +276,31 @@
|
|
|
249
276
|
"projects.color_pink": "Pink",
|
|
250
277
|
"projects.color_yellow": "Yellow",
|
|
251
278
|
"projects.color_gray": "Gray",
|
|
279
|
+
"projects.git_repo_title": "Git repository",
|
|
280
|
+
"projects.step_source": "Source",
|
|
281
|
+
"projects.step_info": "Info",
|
|
282
|
+
"projects.step_languages": "Languages",
|
|
283
|
+
"projects.step_source_hint": "Enter a local path and/or a git repository to auto-detect project configuration.",
|
|
284
|
+
"projects.detecting": "Detecting...",
|
|
285
|
+
"projects.detect_next": "Detect & Next",
|
|
286
|
+
"projects.creating_languages": "Adding languages...",
|
|
287
|
+
"projects.scanning_files": "Scanning source files...",
|
|
288
|
+
"projects.syncing_translations": "Syncing translations...",
|
|
289
|
+
"projects.no_source_hint": "Enter a local path or git repo URL to continue.",
|
|
290
|
+
"projects.step_languages_hint": "Add the languages for this project. The first one will be set as default. You can add more later.",
|
|
291
|
+
"languages.selected": "Selected languages",
|
|
292
|
+
"languages.set_as_default": "Set as default",
|
|
293
|
+
"projects.git_repos_title": "Git repositories",
|
|
294
|
+
"projects.git_token_set": "token set",
|
|
295
|
+
"projects.git_repo_url_label": "Repository URL",
|
|
296
|
+
"projects.git_repo_branch_label": "Branch",
|
|
297
|
+
"projects.git_repo_name_label": "Name (optional)",
|
|
298
|
+
"projects.git_repo_name_placeholder": "e.g. frontend",
|
|
299
|
+
"projects.git_token_label": "Access token (optional)",
|
|
300
|
+
"projects.git_token_placeholder": "ghp_...",
|
|
301
|
+
"projects.git_add_btn": "Add repository",
|
|
302
|
+
"profile.updated": "Account updated",
|
|
303
|
+
"profile.access_updated": "Access updated",
|
|
252
304
|
"profile.member_since": "Member since",
|
|
253
305
|
"profile.last_login": "Last login",
|
|
254
306
|
"profile.edit_account": "Edit account",
|
|
@@ -262,8 +314,11 @@
|
|
|
262
314
|
"profile.name_label": "Full name",
|
|
263
315
|
"profile.name_placeholder": "John Doe",
|
|
264
316
|
"profile.total_translations": "Total translations",
|
|
265
|
-
"profile.
|
|
266
|
-
"profile.
|
|
317
|
+
"profile.period_1d": "Last 24 hours",
|
|
318
|
+
"profile.period_7d": "Last 7 days",
|
|
319
|
+
"profile.period_30d": "Last 30 days",
|
|
320
|
+
"profile.period_365d": "Last year",
|
|
321
|
+
"profile.period_all": "Since account creation",
|
|
267
322
|
"profile.general": "General",
|
|
268
323
|
"profile.name_email_required": "Name and email are required",
|
|
269
324
|
"dashboard.title": "Dashboard",
|
|
@@ -290,6 +345,7 @@
|
|
|
290
345
|
"dashboard.languages_coverage": "Language coverage",
|
|
291
346
|
"dashboard.no_languages": "No language configured",
|
|
292
347
|
"dashboard.missing": "missing",
|
|
348
|
+
"common.errors": "errors",
|
|
293
349
|
"common.cancel": "Cancel",
|
|
294
350
|
"common.save": "Save",
|
|
295
351
|
"common.delete": "Delete",
|
|
@@ -301,6 +357,8 @@
|
|
|
301
357
|
"common.create": "Create",
|
|
302
358
|
"common.or": "or",
|
|
303
359
|
"common.irreversible": "This action is irreversible.",
|
|
360
|
+
"common.back": "Back",
|
|
361
|
+
"common.next": "Next",
|
|
304
362
|
"common.just_now": "just now",
|
|
305
363
|
"common.ago": "ago",
|
|
306
364
|
"login.title": "Log in",
|
|
@@ -361,15 +419,28 @@
|
|
|
361
419
|
"pathpicker.title": "Select a folder",
|
|
362
420
|
"pathpicker.empty": "No subfolder",
|
|
363
421
|
"pathpicker.select": "Select this folder",
|
|
422
|
+
"sync.toast_title": "Sync",
|
|
423
|
+
"sync.added": "added",
|
|
424
|
+
"sync.updated": "updated",
|
|
425
|
+
"sync.total": "total",
|
|
426
|
+
"scan.toast_title": "Scan",
|
|
427
|
+
"scan.langs_added": "language(s) added",
|
|
364
428
|
"scan.modal_title": "Scan project",
|
|
365
429
|
"scan.mode_local": "Local",
|
|
366
|
-
"scan.
|
|
430
|
+
"scan.mode_git": "Git repo",
|
|
367
431
|
"scan.local_hint": "Select the root folder of your Vue.js project. The scanner will detect all $t(), t(), <i18n-t> and v-t usages.",
|
|
368
432
|
"scan.local_path_label": "Project root folder",
|
|
369
|
-
"scan.
|
|
370
|
-
"scan.
|
|
371
|
-
"scan.
|
|
372
|
-
"scan.
|
|
433
|
+
"scan.git_hint": "Enter the URL of a Git repository. The dashboard will clone it and scan source files for translation keys.",
|
|
434
|
+
"scan.git_url_label": "Repository URL",
|
|
435
|
+
"scan.git_url_hint": "Example: https://github.com/user/my-app",
|
|
436
|
+
"scan.git_branch_label": "Branch",
|
|
437
|
+
"scan.git_branch_hint": "Leave empty to use the default branch",
|
|
438
|
+
"scan.git_token_label": "Access token",
|
|
439
|
+
"scan.git_token_hint": "For private repositories",
|
|
440
|
+
"scan.git_save": "Save this repository to the project",
|
|
441
|
+
"scan.git_saved": "Saved repositories",
|
|
442
|
+
"scan.git_repo_name_label": "Repository name",
|
|
443
|
+
"scan.git_repo_name_placeholder": "My App",
|
|
373
444
|
"scan.results": "Results",
|
|
374
445
|
"scan.keys_found": "keys found",
|
|
375
446
|
"scan.keys_added": "new keys",
|
|
@@ -377,19 +448,5 @@
|
|
|
377
448
|
"scan.files_scanned": "files scanned",
|
|
378
449
|
"scan.errors": "errors",
|
|
379
450
|
"scan.run": "Scan",
|
|
380
|
-
"scan.
|
|
381
|
-
"scan.git_hint": "Enter the Git repository URL. The scanner will clone it and detect all translation key usages in source files.",
|
|
382
|
-
"scan.git_url_label": "Repository URL",
|
|
383
|
-
"scan.git_url_hint": "Example: https://github.com/user/repo.git",
|
|
384
|
-
"scan.git_branch_label": "Branch",
|
|
385
|
-
"scan.git_branch_hint": "Leave empty to use main",
|
|
386
|
-
"scan.git_token_label": "Access token",
|
|
387
|
-
"scan.git_token_hint": "Required for private repositories",
|
|
388
|
-
"scan.git_token_placeholder": "ghp_xxxxxxxxxxxx",
|
|
389
|
-
"settings.git_url": "Git repository URL",
|
|
390
|
-
"settings.git_url_hint": "Used for Git scan mode",
|
|
391
|
-
"settings.git_branch": "Git branch",
|
|
392
|
-
"settings.git_branch_hint": "Default: main",
|
|
393
|
-
"settings.git_token": "Git access token",
|
|
394
|
-
"settings.git_token_hint": "For private repositories"
|
|
451
|
+
"scan.translations_synced": "translations imported"
|
|
395
452
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-2">
|
|
3
|
+
<div class="grid grid-cols-2 gap-2">
|
|
4
|
+
<UFormField :label="t('projects.git_repo_url_label', 'Repository URL')" class="col-span-2">
|
|
5
|
+
<UInput v-model="local.url" class="w-full" placeholder="https://github.com/org/repo.git" @input="emitCurrent" />
|
|
6
|
+
</UFormField>
|
|
7
|
+
<UFormField :label="t('projects.git_repo_branch_label', 'Branch')" class="col-span-2">
|
|
8
|
+
<UInput v-model="local.branch" class="w-full" placeholder="main" @input="emitCurrent" />
|
|
9
|
+
</UFormField>
|
|
10
|
+
</div>
|
|
11
|
+
<UFormField :label="t('projects.git_token_label', 'Access token (optional)')">
|
|
12
|
+
<UInput v-model="local.token" type="password" class="w-full" :placeholder="t('projects.git_token_placeholder', 'ghp_...')" @input="emitCurrent" />
|
|
13
|
+
</UFormField>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { GitRepo } from '~/interfaces/project.interface'
|
|
19
|
+
|
|
20
|
+
const { t } = useT()
|
|
21
|
+
|
|
22
|
+
const props = defineProps<{
|
|
23
|
+
modelValue: GitRepo | null | undefined
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
'update:modelValue': [value: GitRepo | null]
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const local = reactive<GitRepo>({ url: '', branch: '', token: '' })
|
|
31
|
+
|
|
32
|
+
watch(() => props.modelValue, (val) => {
|
|
33
|
+
local.url = val?.url ?? ''
|
|
34
|
+
local.branch = val?.branch ?? ''
|
|
35
|
+
local.token = val?.token ?? ''
|
|
36
|
+
}, { immediate: true })
|
|
37
|
+
|
|
38
|
+
function emitCurrent() {
|
|
39
|
+
emit('update:modelValue', local.url.trim() ? { ...local } : null)
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-3">
|
|
3
|
+
<!-- Search -->
|
|
4
|
+
<UInput
|
|
5
|
+
v-model="search"
|
|
6
|
+
:placeholder="t('onboarding.languages_search', 'Search for a language...')"
|
|
7
|
+
icon="i-heroicons-magnifying-glass"
|
|
8
|
+
class="w-full"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<!-- List -->
|
|
12
|
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
13
|
+
<div class="max-h-48 overflow-y-auto">
|
|
14
|
+
<button
|
|
15
|
+
v-for="lang in filteredList"
|
|
16
|
+
:key="lang.code"
|
|
17
|
+
class="w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
18
|
+
:class="isSelected(lang.code) ? 'opacity-40 cursor-default' : 'text-gray-700 dark:text-gray-300'"
|
|
19
|
+
:disabled="isSelected(lang.code)"
|
|
20
|
+
@click="add(lang)"
|
|
21
|
+
>
|
|
22
|
+
<span class="font-mono text-xs text-gray-400 w-14 shrink-0">{{ lang.code }}</span>
|
|
23
|
+
<span class="flex-1">{{ lang.nativeName }}</span>
|
|
24
|
+
<span class="text-xs text-gray-400 shrink-0">{{ lang.name }}</span>
|
|
25
|
+
<UIcon v-if="isSelected(lang.code)" name="i-heroicons-check" class="text-primary-500 shrink-0" />
|
|
26
|
+
</button>
|
|
27
|
+
<div v-if="!filteredList.length" class="px-3 py-4 text-sm text-center text-gray-400">
|
|
28
|
+
{{ t('languages.none_found', 'No language found') }}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Custom BCP-47 -->
|
|
33
|
+
<div
|
|
34
|
+
v-if="search && isValidBcp47(search) && !filteredList.find(l => l.code.toLowerCase() === search.toLowerCase())"
|
|
35
|
+
class="border-t border-gray-200 dark:border-gray-700"
|
|
36
|
+
>
|
|
37
|
+
<button
|
|
38
|
+
class="w-full flex items-center gap-3 px-3 py-2.5 text-sm text-left hover:bg-amber-50 dark:hover:bg-amber-900/20 text-gray-500 dark:text-gray-400 transition-colors"
|
|
39
|
+
@click="addCustom(search)"
|
|
40
|
+
>
|
|
41
|
+
<UIcon name="i-heroicons-plus-circle" class="shrink-0 text-amber-500" />
|
|
42
|
+
<span class="flex-1">{{ t('languages.use_code', 'Use code') }} <code class="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">{{ search }}</code></span>
|
|
43
|
+
<UBadge size="xs" color="warning" variant="soft">BCP 47</UBadge>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Selected languages -->
|
|
49
|
+
<div v-if="modelValue.length" class="space-y-1.5">
|
|
50
|
+
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('languages.selected', 'Selected languages') }}</p>
|
|
51
|
+
<div class="flex flex-wrap gap-2">
|
|
52
|
+
<div
|
|
53
|
+
v-for="lang in modelValue"
|
|
54
|
+
:key="lang.code"
|
|
55
|
+
class="flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 rounded-lg px-2 py-1"
|
|
56
|
+
>
|
|
57
|
+
<span class="text-xs font-mono font-medium text-gray-700 dark:text-gray-300">{{ lang.code }}</span>
|
|
58
|
+
<UBadge
|
|
59
|
+
v-if="lang.is_default"
|
|
60
|
+
size="xs"
|
|
61
|
+
color="primary"
|
|
62
|
+
variant="soft"
|
|
63
|
+
class="cursor-pointer"
|
|
64
|
+
@click="setDefault(lang.code)"
|
|
65
|
+
>{{ t('languages.default_badge', 'Default') }}</UBadge>
|
|
66
|
+
<button
|
|
67
|
+
v-else
|
|
68
|
+
class="text-xs text-gray-400 hover:text-primary-500 transition-colors"
|
|
69
|
+
:title="t('languages.set_as_default', 'Set as default')"
|
|
70
|
+
@click="setDefault(lang.code)"
|
|
71
|
+
>
|
|
72
|
+
<UIcon name="i-heroicons-star" class="text-xs" />
|
|
73
|
+
</button>
|
|
74
|
+
<button class="text-gray-300 hover:text-red-500 transition-colors" @click="remove(lang.code)">
|
|
75
|
+
<UIcon name="i-heroicons-x-mark" class="text-xs" />
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { LANGUAGES } from '~/consts/languages.const'
|
|
85
|
+
|
|
86
|
+
const { t } = useT()
|
|
87
|
+
|
|
88
|
+
const props = defineProps<{
|
|
89
|
+
modelValue: Array<{ code: string; name: string; is_default: boolean }>
|
|
90
|
+
}>()
|
|
91
|
+
|
|
92
|
+
const emit = defineEmits<{
|
|
93
|
+
'update:modelValue': [value: Array<{ code: string; name: string; is_default: boolean }>]
|
|
94
|
+
}>()
|
|
95
|
+
|
|
96
|
+
const search = ref('')
|
|
97
|
+
|
|
98
|
+
const filteredList = computed(() => {
|
|
99
|
+
const q = search.value.toLowerCase()
|
|
100
|
+
const list = q
|
|
101
|
+
? LANGUAGES.filter(l =>
|
|
102
|
+
l.code.toLowerCase().includes(q)
|
|
103
|
+
|| l.name.toLowerCase().includes(q)
|
|
104
|
+
|| l.nativeName.toLowerCase().includes(q),
|
|
105
|
+
)
|
|
106
|
+
: LANGUAGES
|
|
107
|
+
return list
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
function isSelected(code: string) {
|
|
111
|
+
return props.modelValue.some(l => l.code === code)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isValidBcp47(code: string): boolean {
|
|
115
|
+
return /^[a-z]{2,8}(-[A-Za-z0-9]{1,8})*$/i.test(code) && code.length >= 2
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function add(lang: { code: string; name: string; nativeName: string }) {
|
|
119
|
+
if (isSelected(lang.code)) return
|
|
120
|
+
const isFirst = props.modelValue.length === 0
|
|
121
|
+
emit('update:modelValue', [...props.modelValue, { code: lang.code, name: lang.name, is_default: isFirst }])
|
|
122
|
+
search.value = ''
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addCustom(code: string) {
|
|
126
|
+
const normalized = code.trim()
|
|
127
|
+
if (isSelected(normalized)) return
|
|
128
|
+
const isFirst = props.modelValue.length === 0
|
|
129
|
+
emit('update:modelValue', [...props.modelValue, { code: normalized, name: normalized, is_default: isFirst }])
|
|
130
|
+
search.value = ''
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function remove(code: string) {
|
|
134
|
+
const updated = props.modelValue.filter(l => l.code !== code)
|
|
135
|
+
// If we removed the default, set the first one as default
|
|
136
|
+
if (updated.length && !updated.some(l => l.is_default)) {
|
|
137
|
+
updated[0].is_default = true
|
|
138
|
+
}
|
|
139
|
+
emit('update:modelValue', updated)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setDefault(code: string) {
|
|
143
|
+
emit('update:modelValue', props.modelValue.map(l => ({ ...l, is_default: l.code === code })))
|
|
144
|
+
}
|
|
145
|
+
</script>
|
package/components/ScanModal.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<UModal v-model:open="open" :title="t('scan.modal_title', 'Scan project')" :ui="{ width: '
|
|
2
|
+
<UModal v-model:open="open" :title="t('scan.modal_title', 'Scan project')" :ui="{ width: '48rem' }">
|
|
3
3
|
<template #body>
|
|
4
4
|
<div class="space-y-5">
|
|
5
5
|
|
|
@@ -29,24 +29,16 @@
|
|
|
29
29
|
</UFormField>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
<!--
|
|
33
|
-
<div v-if="mode === '
|
|
32
|
+
<!-- Git mode -->
|
|
33
|
+
<div v-if="mode === 'git'" class="space-y-4">
|
|
34
34
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
35
|
-
{{ t('scan.
|
|
35
|
+
{{ t('scan.git_hint', 'Clone a Git repository and scan source files for translation keys.') }}
|
|
36
36
|
</p>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{{ t('scan.url_no_langs', 'No languages configured on this project yet. Add languages first so the scanner knows which locale files to fetch.') }}
|
|
43
|
-
</div>
|
|
44
|
-
<div v-else class="flex flex-wrap gap-1.5">
|
|
45
|
-
<UBadge v-for="lang in project.languages" :key="lang.code" color="neutral" variant="soft" size="xs">
|
|
46
|
-
<UIcon name="i-heroicons-document-text" class="mr-1 opacity-60" />
|
|
47
|
-
{{ lang.code }}.json
|
|
48
|
-
</UBadge>
|
|
49
|
-
</div>
|
|
37
|
+
<GitRepoManager v-model="gitRepo" />
|
|
38
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
39
|
+
<UToggle v-model="saveRepo" size="sm" />
|
|
40
|
+
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('scan.git_save', 'Save this repository to the project') }}</span>
|
|
41
|
+
</label>
|
|
50
42
|
</div>
|
|
51
43
|
|
|
52
44
|
<!-- Results -->
|
|
@@ -69,6 +61,14 @@
|
|
|
69
61
|
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ result.scannedFiles }}</p>
|
|
70
62
|
<p class="text-xs text-gray-400">{{ t('scan.files_scanned', 'files scanned') }}</p>
|
|
71
63
|
</div>
|
|
64
|
+
<div v-if="result.langsAdded" class="bg-white dark:bg-gray-900 rounded-lg p-2.5 text-center">
|
|
65
|
+
<p class="text-xl font-bold text-primary-600 dark:text-primary-400">{{ result.langsAdded }}</p>
|
|
66
|
+
<p class="text-xs text-gray-400">{{ t('scan.langs_added', 'language(s) added') }}</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div v-if="result.translationsAdded !== undefined" class="bg-white dark:bg-gray-900 rounded-lg p-2.5 text-center col-span-2">
|
|
69
|
+
<p class="text-xl font-bold text-green-600 dark:text-green-400">{{ result.translationsAdded }}</p>
|
|
70
|
+
<p class="text-xs text-gray-400">{{ t('scan.translations_synced', 'translations imported') }}</p>
|
|
71
|
+
</div>
|
|
72
72
|
</div>
|
|
73
73
|
<p v-if="result.errors?.length" class="text-xs text-red-500">
|
|
74
74
|
{{ result.errors.length }} {{ t('scan.errors', 'errors') }}
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
<UButton color="neutral" variant="ghost" @click="open = false">{{ t('common.cancel', 'Cancel') }}</UButton>
|
|
85
85
|
<UButton
|
|
86
86
|
:loading="loading"
|
|
87
|
-
:disabled="mode === 'local' ? !localPath : !
|
|
87
|
+
:disabled="mode === 'local' ? !localPath : !gitRepo?.url"
|
|
88
88
|
icon="i-heroicons-magnifying-glass"
|
|
89
89
|
@click="runScan"
|
|
90
90
|
>
|
|
@@ -96,27 +96,30 @@
|
|
|
96
96
|
</template>
|
|
97
97
|
|
|
98
98
|
<script setup lang="ts">
|
|
99
|
+
import type { GitRepo } from '~/interfaces/project.interface'
|
|
100
|
+
|
|
99
101
|
const { t } = useT()
|
|
100
102
|
|
|
101
103
|
const props = defineProps<{
|
|
102
104
|
projectId: number
|
|
103
|
-
project?: { languages?: { code: string; name: string }[]; root_path?: string;
|
|
105
|
+
project?: { languages?: { code: string; name: string }[]; root_path?: string; git_repo?: GitRepo | null }
|
|
104
106
|
}>()
|
|
105
107
|
|
|
106
108
|
const emit = defineEmits<{ done: [] }>()
|
|
107
109
|
|
|
108
110
|
const open = defineModel<boolean>('open', { default: false })
|
|
109
111
|
|
|
110
|
-
const mode = ref<'local' | '
|
|
112
|
+
const mode = ref<'local' | 'git'>('local')
|
|
111
113
|
const localPath = ref(props.project?.root_path ?? '')
|
|
112
|
-
const
|
|
114
|
+
const gitRepo = ref<GitRepo | null>(null)
|
|
115
|
+
const saveRepo = ref(false)
|
|
113
116
|
const loading = ref(false)
|
|
114
117
|
const result = ref<any>(null)
|
|
115
118
|
const error = ref('')
|
|
116
119
|
|
|
117
120
|
const modes = computed(() => [
|
|
118
121
|
{ value: 'local', label: t('scan.mode_local', 'Local'), icon: 'i-heroicons-computer-desktop' },
|
|
119
|
-
{ value: '
|
|
122
|
+
{ value: 'git', label: t('scan.mode_git', 'Git repo'), icon: 'i-heroicons-code-bracket' },
|
|
120
123
|
])
|
|
121
124
|
|
|
122
125
|
watch(open, (val) => {
|
|
@@ -124,8 +127,9 @@ watch(open, (val) => {
|
|
|
124
127
|
result.value = null
|
|
125
128
|
error.value = ''
|
|
126
129
|
localPath.value = props.project?.root_path ?? ''
|
|
127
|
-
|
|
128
|
-
mode.value = props.project?.root_path ? 'local' :
|
|
130
|
+
saveRepo.value = false
|
|
131
|
+
mode.value = props.project?.root_path ? 'local' : 'git'
|
|
132
|
+
gitRepo.value = props.project?.git_repo ? { ...props.project.git_repo } : null
|
|
129
133
|
}
|
|
130
134
|
})
|
|
131
135
|
|
|
@@ -134,13 +138,22 @@ async function runScan() {
|
|
|
134
138
|
error.value = ''
|
|
135
139
|
result.value = null
|
|
136
140
|
try {
|
|
141
|
+
if (mode.value === 'git' && saveRepo.value && gitRepo.value?.url) {
|
|
142
|
+
await $fetch(`/api/projects/${props.projectId}`, {
|
|
143
|
+
method: 'PUT',
|
|
144
|
+
body: { git_repo: gitRepo.value },
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
result.value = await $fetch('/api/scan', {
|
|
138
149
|
method: 'POST',
|
|
139
150
|
body: {
|
|
140
151
|
project_id: props.projectId,
|
|
141
152
|
mode: mode.value,
|
|
142
153
|
root_path: mode.value === 'local' ? localPath.value : undefined,
|
|
143
|
-
|
|
154
|
+
git_url: mode.value === 'git' ? gitRepo.value?.url : undefined,
|
|
155
|
+
git_branch: mode.value === 'git' ? gitRepo.value?.branch : undefined,
|
|
156
|
+
git_token: mode.value === 'git' ? gitRepo.value?.token : undefined,
|
|
144
157
|
},
|
|
145
158
|
})
|
|
146
159
|
emit('done')
|
|
@@ -143,8 +143,11 @@
|
|
|
143
143
|
<!-- Status dot -->
|
|
144
144
|
<UTooltip :text="statusLabel(lang.code)" :delay-duration="300">
|
|
145
145
|
<span
|
|
146
|
-
class="mt-1.5 w-2 h-2 rounded-full shrink-0"
|
|
147
|
-
:class="[
|
|
146
|
+
class="mt-1.5 w-2 h-2 rounded-full shrink-0 transition-opacity"
|
|
147
|
+
:class="[
|
|
148
|
+
cyclingStatusLang === lang.code ? 'bg-gray-300 dark:bg-gray-600 animate-pulse cursor-wait' : statusDot(lang.code),
|
|
149
|
+
canClickStatus(lang.code) && cyclingStatusLang !== lang.code ? 'cursor-pointer' : 'cursor-default'
|
|
150
|
+
]"
|
|
148
151
|
@click="cycleStatus(lang.code)"
|
|
149
152
|
/>
|
|
150
153
|
</UTooltip>
|
|
@@ -275,6 +278,8 @@ const editingCell = ref<string | null>(null)
|
|
|
275
278
|
const editValues = ref<Record<string, string>>({})
|
|
276
279
|
const saving = ref<string | null>(null)
|
|
277
280
|
const translateLoading = ref<string | null>(null)
|
|
281
|
+
const cyclingStatusLang = ref<string | null>(null)
|
|
282
|
+
const deletingKey = ref(false)
|
|
278
283
|
const showHistory = ref(false)
|
|
279
284
|
const historyTranslationId = ref<number | null>(null)
|
|
280
285
|
const textareaWrappers = ref<Record<string, HTMLElement | null>>({})
|
|
@@ -387,7 +392,7 @@ function canClickStatus(langCode: string): boolean {
|
|
|
387
392
|
|
|
388
393
|
async function cycleStatus(langCode: string) {
|
|
389
394
|
const tr = props.translationKey.translations[langCode]
|
|
390
|
-
if (!tr?.value) return
|
|
395
|
+
if (!tr?.value || cyclingStatusLang.value) return
|
|
391
396
|
|
|
392
397
|
const current = (tr.status as TRANSLATION_STATUS) || TRANSLATION_STATUS.DRAFT
|
|
393
398
|
let next: TRANSLATION_STATUS
|
|
@@ -400,6 +405,7 @@ async function cycleStatus(langCode: string) {
|
|
|
400
405
|
next = current === TRANSLATION_STATUS.DRAFT ? TRANSLATION_STATUS.REVIEWED : TRANSLATION_STATUS.DRAFT
|
|
401
406
|
}
|
|
402
407
|
|
|
408
|
+
cyclingStatusLang.value = langCode
|
|
403
409
|
try {
|
|
404
410
|
await $fetch('/api/translations/status', {
|
|
405
411
|
method: 'POST',
|
|
@@ -409,6 +415,8 @@ async function cycleStatus(langCode: string) {
|
|
|
409
415
|
refreshNuxtData('project-stats')
|
|
410
416
|
} catch (e: any) {
|
|
411
417
|
toast.add({ title: t('common.error', 'Error'), description: e.data?.message || e.message, color: 'error' })
|
|
418
|
+
} finally {
|
|
419
|
+
cyclingStatusLang.value = null
|
|
412
420
|
}
|
|
413
421
|
}
|
|
414
422
|
|
|
@@ -529,6 +537,7 @@ const rowActions = computed(() => {
|
|
|
529
537
|
})
|
|
530
538
|
|
|
531
539
|
async function deleteKey() {
|
|
540
|
+
deletingKey.value = true
|
|
532
541
|
try {
|
|
533
542
|
await $fetch(`/api/keys/${props.translationKey.id}`, { method: 'DELETE' })
|
|
534
543
|
toast.add({ title: t('translations.key_deleted', 'Key deleted'), color: 'success' })
|
|
@@ -536,6 +545,8 @@ async function deleteKey() {
|
|
|
536
545
|
refreshNuxtData('project-stats')
|
|
537
546
|
} catch (e: any) {
|
|
538
547
|
toast.add({ title: t('common.error', 'Error'), description: e.data?.message || e.message, color: 'error' })
|
|
548
|
+
} finally {
|
|
549
|
+
deletingKey.value = false
|
|
539
550
|
}
|
|
540
551
|
}
|
|
541
552
|
</script>
|