i18n-dashboard 0.13.2 → 0.15.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 +18 -2
- package/nuxt.config.ts +13 -0
- package/package.json +13 -6
- package/src/app.vue +6 -2
- package/src/assets/locales/en.json +102 -1
- package/src/components/GitRepoManager.vue +28 -6
- package/src/components/LanguagePicker.vue +43 -11
- package/src/components/LinkedKeyPicker.vue +21 -5
- package/src/components/PathPicker.vue +61 -15
- package/src/components/PluralEditor.vue +40 -12
- package/src/components/ScanModal.vue +102 -29
- package/src/components/TranslationHistoryModal.vue +42 -10
- package/src/components/TranslationRow.vue +86 -21
- package/src/components/dashboard/WidgetConfigModal.vue +23 -8
- package/src/components/dashboard/WidgetGrid.vue +40 -11
- package/src/components/dashboard/WidgetPicker.vue +24 -8
- package/src/components/dashboard/widgets/ActivityWidget.vue +54 -15
- package/src/components/dashboard/widgets/LanguagesCoverageWidget.vue +54 -15
- package/src/components/dashboard/widgets/ProjectsWidget.vue +36 -9
- package/src/components/dashboard/widgets/ReviewWidget.vue +66 -18
- package/src/components/dashboard/widgets/StatWidget.vue +34 -9
- package/src/composables/useAuth.ts +3 -1
- package/src/composables/useProfile.ts +1 -1
- package/src/composables/useProject.ts +3 -3
- package/src/composables/useReview.ts +54 -7
- package/src/composables/useWidgetData.ts +1 -1
- package/src/consts/commons.const.ts +10 -0
- package/src/interfaces/translation-job.interface.ts +1 -1
- package/src/layouts/default.vue +218 -46
- package/src/middleware/auth.global.ts +30 -9
- package/src/pages/admin/logs.vue +272 -0
- package/src/pages/admin/security.vue +197 -0
- package/src/pages/admin/smtp.vue +382 -0
- package/src/pages/forgot-password.vue +128 -0
- package/src/pages/index.vue +4 -1
- package/src/pages/login.vue +59 -9
- package/src/pages/onboarding.vue +409 -77
- package/src/pages/projects/[id]/formats/datetime.vue +172 -36
- package/src/pages/projects/[id]/formats/modifiers.vue +148 -32
- package/src/pages/projects/[id]/formats/number.vue +190 -38
- package/src/pages/projects/[id]/index.vue +200 -39
- package/src/pages/projects/[id]/languages.vue +289 -67
- package/src/pages/projects/[id]/review.vue +189 -64
- package/src/pages/projects/[id]/settings.vue +264 -97
- package/src/pages/projects/[id]/translations/[keyId].vue +533 -314
- package/src/pages/projects/[id]/translations/index.vue +171 -36
- package/src/pages/projects/[id]/users.vue +261 -60
- package/src/pages/projects/index.vue +409 -105
- package/src/pages/reset-password.vue +164 -0
- package/src/pages/users/[id]/profile.vue +267 -60
- package/src/pages/users/index.vue +237 -62
- package/src/server/api/admin/logs.delete.ts +19 -0
- package/src/server/api/admin/logs.get.ts +19 -0
- package/src/server/api/admin/smtp-test.post.ts +28 -0
- package/src/server/api/admin/smtp.get.ts +25 -0
- package/src/server/api/admin/smtp.post.ts +37 -0
- package/src/server/api/auth/forgot-password.post.ts +49 -0
- package/src/server/api/auth/login.post.ts +1 -1
- package/src/server/api/auth/logout.post.ts +1 -1
- package/src/server/api/auth/me.get.ts +1 -2
- package/src/server/api/auth/me.put.ts +1 -1
- package/src/server/api/auth/password.put.ts +10 -4
- package/src/server/api/auth/refresh.post.ts +1 -1
- package/src/server/api/auth/reset-password.post.ts +41 -0
- package/src/server/api/auth/status.get.ts +14 -9
- package/src/server/api/config.get.ts +1 -1
- package/src/server/api/dashboard/layout.get.ts +1 -1
- package/src/server/api/dashboard/layout.post.ts +1 -1
- package/src/server/api/export.get.ts +1 -1
- package/src/server/api/keys/index.get.ts +27 -9
- package/src/server/api/languages/index.post.ts +1 -1
- package/src/server/api/onboarding.post.ts +1 -1
- package/src/server/api/profile.get.ts +1 -1
- package/src/server/api/project-snapshot.post.ts +1 -1
- package/src/server/api/projects/detect.post.ts +1 -1
- package/src/server/api/scan.post.ts +1 -1
- package/src/server/api/settings/password-policy.get.ts +7 -0
- package/src/server/api/setup.post.ts +10 -4
- package/src/server/api/stats/global.get.ts +1 -1
- package/src/server/api/translations/batch-translate.post.ts +7 -0
- package/src/server/api/translations/bulk-status.post.ts +1 -1
- package/src/server/api/translations/index.post.ts +1 -1
- package/src/server/api/translations/job/[id].get.ts +1 -1
- package/src/server/api/translations/status.post.ts +1 -1
- package/src/server/api/translations/translate-all.post.ts +1 -1
- package/src/server/api/users/[id]/profile.get.ts +2 -2
- package/src/server/api/users/[id]/roles.put.ts +1 -1
- package/src/server/api/users/[id].delete.ts +1 -1
- package/src/server/api/users/[id].put.ts +1 -1
- package/src/server/api/users/index.get.ts +1 -1
- package/src/server/api/users/index.post.ts +11 -6
- package/src/server/db/index.ts +129 -11
- package/src/server/middleware/auth.ts +11 -6
- package/src/server/routes/locale/[lang].get.ts +1 -1
- package/src/server/tasks/purge-logs.ts +44 -0
- package/src/server/utils/auth.util.ts +10 -9
- package/src/server/utils/auto-translate.util.ts +1 -1
- package/src/server/utils/log.util.ts +21 -0
- package/src/server/utils/mailer.util.ts +70 -11
- package/src/server/utils/password.util.ts +46 -0
- package/src/server/utils/project-config.util.ts +2 -2
- package/src/server/utils/scanner.util.ts +3 -3
- package/src/server/utils/translation-job.util.ts +3 -3
- package/src/services/base.service.ts +11 -1
- package/src/services/profile.service.ts +1 -1
- package/src/types/auth.type.ts +1 -1
- package/i18n-dashboard.config.example.js +0 -40
- package/src/server/consts/commons.const.ts +0 -6
- /package/src/{server/consts → consts}/db.const.ts +0 -0
package/README.md
CHANGED
|
@@ -77,6 +77,19 @@ npx i18n-dashboard start
|
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
80
|
+
## 🤔 Why I built this
|
|
81
|
+
|
|
82
|
+
Editing i18n JSON files was breaking my flow.
|
|
83
|
+
|
|
84
|
+
I wanted something that:
|
|
85
|
+
- works with vue-i18n
|
|
86
|
+
- doesn't require migration
|
|
87
|
+
- stays simple but scalable
|
|
88
|
+
|
|
89
|
+
So I built this.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
80
93
|
## 📘 Documentation
|
|
81
94
|
|
|
82
95
|
- See USAGE.md for workflows
|
|
@@ -86,8 +99,11 @@ npx i18n-dashboard start
|
|
|
86
99
|
|
|
87
100
|
## 💬 Feedback
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
If you're using vue-i18n, I'd genuinely love your feedback.
|
|
103
|
+
|
|
104
|
+
This tool comes from a real pain point, and I'm trying to improve it.
|
|
105
|
+
|
|
106
|
+
Feel free to open an issue or share your thoughts.
|
|
91
107
|
|
|
92
108
|
---
|
|
93
109
|
|
package/nuxt.config.ts
CHANGED
|
@@ -15,6 +15,10 @@ export default defineNuxtConfig({
|
|
|
15
15
|
compatibilityDate: '2025-01-01',
|
|
16
16
|
devtools: { enabled: false },
|
|
17
17
|
|
|
18
|
+
app: {
|
|
19
|
+
pageTransition: { name: 'page', mode: 'out-in' },
|
|
20
|
+
},
|
|
21
|
+
|
|
18
22
|
srcDir: 'src',
|
|
19
23
|
serverDir: 'src/server',
|
|
20
24
|
|
|
@@ -41,6 +45,11 @@ export default defineNuxtConfig({
|
|
|
41
45
|
localesPath: process.env.I18N_LOCALES_PATH || 'src/locales',
|
|
42
46
|
// Auth — set SESSION_SECRET to a strong random value in production
|
|
43
47
|
sessionSecret: process.env.SESSION_SECRET || DEFAULT_SECRET,
|
|
48
|
+
// Security — tunable without code changes
|
|
49
|
+
bcryptRounds: process.env.BCRYPT_ROUNDS || '12',
|
|
50
|
+
sessionTtlMinutes: process.env.SESSION_TTL_MINUTES || '15',
|
|
51
|
+
refreshTokenTtlDays: process.env.REFRESH_TOKEN_TTL_DAYS || '7',
|
|
52
|
+
resetTokenTtlHours: process.env.RESET_TOKEN_TTL_HOURS || '1',
|
|
44
53
|
// Email (SMTP) — optional
|
|
45
54
|
smtpHost: process.env.SMTP_HOST || '',
|
|
46
55
|
smtpPort: process.env.SMTP_PORT || '587',
|
|
@@ -69,6 +78,10 @@ export default defineNuxtConfig({
|
|
|
69
78
|
dir: './src/assets/locales',
|
|
70
79
|
},
|
|
71
80
|
],
|
|
81
|
+
experimental: { tasks: true },
|
|
82
|
+
scheduledTasks: {
|
|
83
|
+
'0 * * * *': ['purge-logs'],
|
|
84
|
+
},
|
|
72
85
|
// ── Security headers ─────────────────────────────────────────────────────
|
|
73
86
|
// Applied to every response from the Nitro server.
|
|
74
87
|
routeRules: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "A web dashboard to manage vue-i18n translation keys with database persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
"stop": "node bin/cli.mjs stop",
|
|
17
17
|
"init": "node bin/cli.mjs init",
|
|
18
18
|
"sync": "node bin/cli.mjs sync",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
21
21
|
"test": "vitest",
|
|
22
22
|
"test:ui": "vitest --ui",
|
|
23
23
|
"test:run": "vitest run",
|
|
24
|
-
"test:coverage": "vitest run --coverage"
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"prepare": "husky"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@nuxt/ui": "^3.3.7",
|
|
@@ -77,11 +78,17 @@
|
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@iconify-json/heroicons": "^1.2.3",
|
|
79
80
|
"@types/node": "^25.3.5",
|
|
81
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
82
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
80
83
|
"@vitest/coverage-v8": "^4.1.0",
|
|
81
84
|
"@vitest/ui": "^4.1.0",
|
|
85
|
+
"@vue/eslint-config-typescript": "^14.7.0",
|
|
82
86
|
"@vue/test-utils": "^2.4.6",
|
|
83
|
-
"
|
|
87
|
+
"eslint": "^10.0.3",
|
|
88
|
+
"eslint-plugin-vue": "^10.8.0",
|
|
84
89
|
"happy-dom": "^20.8.4",
|
|
85
|
-
"
|
|
90
|
+
"husky": "^9.1.7",
|
|
91
|
+
"vitest": "^4.1.0",
|
|
92
|
+
"vue-eslint-parser": "^10.4.0"
|
|
86
93
|
}
|
|
87
94
|
}
|
package/src/app.vue
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<UApp>
|
|
3
|
-
<NuxtLoadingIndicator
|
|
3
|
+
<NuxtLoadingIndicator
|
|
4
|
+
color="rgb(var(--ui-primary))"
|
|
5
|
+
:height="3"
|
|
6
|
+
:throttle="100"
|
|
7
|
+
/>
|
|
4
8
|
<NuxtLayout>
|
|
5
|
-
<NuxtPage
|
|
9
|
+
<NuxtPage />
|
|
6
10
|
</NuxtLayout>
|
|
7
11
|
</UApp>
|
|
8
12
|
</template>
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"translations.search": "Search for a key...",
|
|
53
53
|
"translations.add_key": "New key",
|
|
54
54
|
"translations.translate_all": "Translate all",
|
|
55
|
+
"translations.nothing_to_translate": "Nothing to translate",
|
|
55
56
|
"translations.no_results": "No keys found",
|
|
56
57
|
"translations.no_results_hint": "Try a different search term.",
|
|
57
58
|
"translations.add_description": "Add a description…",
|
|
@@ -108,6 +109,8 @@
|
|
|
108
109
|
"review.mark_all_reviewed": "Mark all as reviewed",
|
|
109
110
|
"review.reject": "Reject",
|
|
110
111
|
"review.mark_reviewed": "Mark as reviewed",
|
|
112
|
+
"review.awaiting_approval": "Awaiting approval",
|
|
113
|
+
"review.translations_approved": "translation(s) approved",
|
|
111
114
|
"languages.deleted": "Language deleted",
|
|
112
115
|
"languages.translating": "Translating",
|
|
113
116
|
"languages.translate_done": "Translation complete",
|
|
@@ -409,6 +412,11 @@
|
|
|
409
412
|
"onboarding.passwords_mismatch": "Passwords do not match.",
|
|
410
413
|
"onboarding.admin_creation_error": "Error creating administrator account.",
|
|
411
414
|
"onboarding.project_name_path_required": "Project name and path are required.",
|
|
415
|
+
"onboarding.project_name_required": "Project name is required.",
|
|
416
|
+
"onboarding.project_path_required": "Project path is required.",
|
|
417
|
+
"onboarding.project_git_url_required": "Repository URL is required.",
|
|
418
|
+
"onboarding.source_local": "Local path",
|
|
419
|
+
"onboarding.source_git": "Git repository",
|
|
412
420
|
"onboarding.project_creation_error": "Error creating project.",
|
|
413
421
|
"settings.snapshot_title": "Project snapshot",
|
|
414
422
|
"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.",
|
|
@@ -456,5 +464,98 @@
|
|
|
456
464
|
"scan.files_scanned": "files scanned",
|
|
457
465
|
"scan.errors": "errors",
|
|
458
466
|
"scan.run": "Scan",
|
|
459
|
-
"scan.translations_synced": "translations imported"
|
|
467
|
+
"scan.translations_synced": "translations imported",
|
|
468
|
+
|
|
469
|
+
"forgot_password.title": "Forgot password",
|
|
470
|
+
"forgot_password.description": "Enter your email address and we'll send you a link to reset your password.",
|
|
471
|
+
"forgot_password.submit": "Send link",
|
|
472
|
+
"forgot_password.back_to_login": "Back to login",
|
|
473
|
+
"forgot_password.success": "If this email is associated with an account, a reset link has been sent to you.",
|
|
474
|
+
"forgot_password.error_fallback": "An error occurred",
|
|
475
|
+
|
|
476
|
+
"reset_password.title": "New password",
|
|
477
|
+
"reset_password.invalid_link": "Invalid link. Please make a new reset request.",
|
|
478
|
+
"reset_password.request_new_link": "Request a new link",
|
|
479
|
+
"reset_password.success": "Password changed successfully.",
|
|
480
|
+
"reset_password.login": "Sign in",
|
|
481
|
+
"reset_password.label_new_password": "New password",
|
|
482
|
+
"reset_password.hint_min_length": "Minimum 8 characters",
|
|
483
|
+
"reset_password.label_confirm": "Confirm password",
|
|
484
|
+
"reset_password.submit": "Change password",
|
|
485
|
+
"reset_password.error_mismatch": "Passwords do not match",
|
|
486
|
+
"reset_password.error_too_short": "Password must be at least 8 characters",
|
|
487
|
+
"reset_password.error_fallback": "An error occurred",
|
|
488
|
+
|
|
489
|
+
"security.title": "Security",
|
|
490
|
+
"security.description": "Password policy and security settings",
|
|
491
|
+
"security.password_policy_title": "Password policy",
|
|
492
|
+
"security.min_length_label": "Minimum length",
|
|
493
|
+
"security.min_length_hint": "Minimum number of characters required",
|
|
494
|
+
"security.complexity_title": "Required complexity",
|
|
495
|
+
"security.require_uppercase": "At least one uppercase letter (A–Z)",
|
|
496
|
+
"security.require_number": "At least one digit (0–9)",
|
|
497
|
+
"security.require_special": "At least one special character (!@#$...)",
|
|
498
|
+
"security.current_rules": "Current rules:",
|
|
499
|
+
"security.rule_min_length_pre": "Minimum",
|
|
500
|
+
"security.rule_character": "character",
|
|
501
|
+
"security.rule_characters": "characters",
|
|
502
|
+
"security.rule_uppercase": "At least one uppercase letter",
|
|
503
|
+
"security.rule_number": "At least one digit",
|
|
504
|
+
"security.rule_special": "At least one special character",
|
|
505
|
+
"security.save": "Save",
|
|
506
|
+
"security.saved": "Settings saved",
|
|
507
|
+
"security.infra_title": "Infrastructure settings",
|
|
508
|
+
"security.infra_description": "These values are configured via environment variables and require a restart.",
|
|
509
|
+
"security.bcrypt_rounds": "Bcrypt rounds",
|
|
510
|
+
"security.session_duration": "Session duration",
|
|
511
|
+
"security.refresh_token_duration": "Refresh token duration",
|
|
512
|
+
"security.reset_link_expiry": "Reset link expiry",
|
|
513
|
+
|
|
514
|
+
"logs.title": "System logs",
|
|
515
|
+
"logs.description": "Error and warning logs generated by background processes",
|
|
516
|
+
"logs.settings_title": "Auto-purge settings",
|
|
517
|
+
"logs.retention_days_label": "Retain logs for (days)",
|
|
518
|
+
"logs.purge_interval_label": "Auto-purge interval (hours)",
|
|
519
|
+
"logs.purge_all": "Purge all",
|
|
520
|
+
"logs.empty": "No logs found",
|
|
521
|
+
"logs.filter_context": "Filter by context...",
|
|
522
|
+
"logs.show_details": "Details",
|
|
523
|
+
"logs.purged_toast": "Logs purged",
|
|
524
|
+
"logs.entries_deleted": "entries deleted",
|
|
525
|
+
"logs.level_all": "All levels",
|
|
526
|
+
"logs.level_error": "Error",
|
|
527
|
+
"logs.level_warn": "Warning",
|
|
528
|
+
"logs.level_info": "Info",
|
|
529
|
+
"logs.nav_label": "System logs",
|
|
530
|
+
"common.refresh": "Refresh",
|
|
531
|
+
"smtp.title": "SMTP configuration",
|
|
532
|
+
"smtp.description": "Configure email sending for invitations and password resets",
|
|
533
|
+
"smtp.host_label": "SMTP host",
|
|
534
|
+
"smtp.port_label": "Port",
|
|
535
|
+
"smtp.secure_label": "Secure connection (TLS/SSL)",
|
|
536
|
+
"smtp.user_label": "Username",
|
|
537
|
+
"smtp.pass_label": "Password",
|
|
538
|
+
"smtp.pass_set_hint": "Password is set — leave blank to keep current",
|
|
539
|
+
"smtp.from_label": "From address",
|
|
540
|
+
"smtp.dashboard_url_label": "Dashboard URL",
|
|
541
|
+
"smtp.dashboard_url_hint": "Used for links in emails",
|
|
542
|
+
"smtp.save": "Save",
|
|
543
|
+
"smtp.test": "Test",
|
|
544
|
+
"smtp.test_email_label": "Send test email to",
|
|
545
|
+
"smtp.not_configured": "SMTP not configured — emails will not be sent",
|
|
546
|
+
"smtp.test_sent": "Test email sent",
|
|
547
|
+
"smtp.test_failed": "Send failed",
|
|
548
|
+
"smtp.saved": "SMTP configuration saved",
|
|
549
|
+
"smtp.nav_label": "SMTP",
|
|
550
|
+
"smtp.config_file_title": "Local config file",
|
|
551
|
+
"smtp.config_file_hint": "For local deployments, you can configure SMTP in a JSON file at your project root. Values are seeded into the database on first start.",
|
|
552
|
+
"smtp.provider_label": "Quick setup",
|
|
553
|
+
"smtp.provider_custom": "Custom",
|
|
554
|
+
"smtp.secure_hint": "Keep off for port 587 (STARTTLS). Enable only for port 465.",
|
|
555
|
+
"smtp.gmail_hint_title": "Gmail requires an App Password",
|
|
556
|
+
"smtp.gmail_step1": "Click this link:",
|
|
557
|
+
"smtp.gmail_step2": "If the page shows \"The setting you are looking for is not available\" → you must first enable 2-Step Verification on your Google account",
|
|
558
|
+
"smtp.gmail_step3": "Type a name (e.g. \"i18n-dashboard\") and click Create",
|
|
559
|
+
"smtp.gmail_step4": "Copy the 16-character code shown (e.g. abcd efgh ijkl mnop)",
|
|
560
|
+
"smtp.gmail_step5": "Paste it in the Password field below — use your Gmail address as Username"
|
|
460
561
|
}
|
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="space-y-2">
|
|
3
3
|
<div class="grid grid-cols-2 gap-2">
|
|
4
|
-
<UFormField
|
|
5
|
-
|
|
4
|
+
<UFormField
|
|
5
|
+
:label="t('projects.git_repo_url_label', 'Repository URL')"
|
|
6
|
+
class="col-span-2"
|
|
7
|
+
>
|
|
8
|
+
<UInput
|
|
9
|
+
v-model="local.url"
|
|
10
|
+
class="w-full"
|
|
11
|
+
placeholder="https://github.com/org/repo.git"
|
|
12
|
+
@input="emitCurrent"
|
|
13
|
+
/>
|
|
6
14
|
</UFormField>
|
|
7
|
-
<UFormField
|
|
8
|
-
|
|
15
|
+
<UFormField
|
|
16
|
+
:label="t('projects.git_repo_branch_label', 'Branch')"
|
|
17
|
+
class="col-span-2"
|
|
18
|
+
>
|
|
19
|
+
<UInput
|
|
20
|
+
v-model="local.branch"
|
|
21
|
+
class="w-full"
|
|
22
|
+
placeholder="main"
|
|
23
|
+
@input="emitCurrent"
|
|
24
|
+
/>
|
|
9
25
|
</UFormField>
|
|
10
26
|
</div>
|
|
11
27
|
<UFormField :label="t('projects.git_token_label', 'Access token (optional)')">
|
|
12
|
-
<UInput
|
|
28
|
+
<UInput
|
|
29
|
+
v-model="local.token"
|
|
30
|
+
type="password"
|
|
31
|
+
class="w-full"
|
|
32
|
+
:placeholder="t('projects.git_token_placeholder', 'ghp_...')"
|
|
33
|
+
@input="emitCurrent"
|
|
34
|
+
/>
|
|
13
35
|
</UFormField>
|
|
14
36
|
</div>
|
|
15
37
|
</template>
|
|
16
38
|
|
|
17
39
|
<script setup lang="ts">
|
|
18
|
-
import type { IGitRepo } from '
|
|
40
|
+
import type { IGitRepo } from '../interfaces/project.interface'
|
|
19
41
|
|
|
20
42
|
const { t } = useT()
|
|
21
43
|
|
|
@@ -22,9 +22,16 @@
|
|
|
22
22
|
<span class="font-mono text-xs text-gray-400 w-14 shrink-0">{{ lang.code }}</span>
|
|
23
23
|
<span class="flex-1">{{ lang.nativeName }}</span>
|
|
24
24
|
<span class="text-xs text-gray-400 shrink-0">{{ lang.name }}</span>
|
|
25
|
-
<UIcon
|
|
25
|
+
<UIcon
|
|
26
|
+
v-if="isSelected(lang.code)"
|
|
27
|
+
name="i-heroicons-check"
|
|
28
|
+
class="text-primary-500 shrink-0"
|
|
29
|
+
/>
|
|
26
30
|
</button>
|
|
27
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
v-if="!filteredList.length"
|
|
33
|
+
class="px-3 py-4 text-sm text-center text-gray-400"
|
|
34
|
+
>
|
|
28
35
|
{{ t('languages.none_found', 'No language found') }}
|
|
29
36
|
</div>
|
|
30
37
|
</div>
|
|
@@ -38,16 +45,30 @@
|
|
|
38
45
|
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
46
|
@click="addCustom(search)"
|
|
40
47
|
>
|
|
41
|
-
<UIcon
|
|
48
|
+
<UIcon
|
|
49
|
+
name="i-heroicons-plus-circle"
|
|
50
|
+
class="shrink-0 text-amber-500"
|
|
51
|
+
/>
|
|
42
52
|
<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
|
|
53
|
+
<UBadge
|
|
54
|
+
size="xs"
|
|
55
|
+
color="warning"
|
|
56
|
+
variant="soft"
|
|
57
|
+
>
|
|
58
|
+
BCP 47
|
|
59
|
+
</UBadge>
|
|
44
60
|
</button>
|
|
45
61
|
</div>
|
|
46
62
|
</div>
|
|
47
63
|
|
|
48
64
|
<!-- Selected languages -->
|
|
49
|
-
<div
|
|
50
|
-
|
|
65
|
+
<div
|
|
66
|
+
v-if="modelValue.length"
|
|
67
|
+
class="space-y-1.5"
|
|
68
|
+
>
|
|
69
|
+
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
70
|
+
{{ t('languages.selected', 'Selected languages') }}
|
|
71
|
+
</p>
|
|
51
72
|
<div class="flex flex-wrap gap-2">
|
|
52
73
|
<div
|
|
53
74
|
v-for="lang in modelValue"
|
|
@@ -62,17 +83,28 @@
|
|
|
62
83
|
variant="soft"
|
|
63
84
|
class="cursor-pointer"
|
|
64
85
|
@click="setDefault(lang.code)"
|
|
65
|
-
>
|
|
86
|
+
>
|
|
87
|
+
{{ t('languages.default_badge', 'Default') }}
|
|
88
|
+
</UBadge>
|
|
66
89
|
<button
|
|
67
90
|
v-else
|
|
68
91
|
class="text-xs text-gray-400 hover:text-primary-500 transition-colors"
|
|
69
92
|
:title="t('languages.set_as_default', 'Set as default')"
|
|
70
93
|
@click="setDefault(lang.code)"
|
|
71
94
|
>
|
|
72
|
-
<UIcon
|
|
95
|
+
<UIcon
|
|
96
|
+
name="i-heroicons-star"
|
|
97
|
+
class="text-xs"
|
|
98
|
+
/>
|
|
73
99
|
</button>
|
|
74
|
-
<button
|
|
75
|
-
|
|
100
|
+
<button
|
|
101
|
+
class="text-gray-300 hover:text-red-500 transition-colors"
|
|
102
|
+
@click="remove(lang.code)"
|
|
103
|
+
>
|
|
104
|
+
<UIcon
|
|
105
|
+
name="i-heroicons-x-mark"
|
|
106
|
+
class="text-xs"
|
|
107
|
+
/>
|
|
76
108
|
</button>
|
|
77
109
|
</div>
|
|
78
110
|
</div>
|
|
@@ -81,7 +113,7 @@
|
|
|
81
113
|
</template>
|
|
82
114
|
|
|
83
115
|
<script setup lang="ts">
|
|
84
|
-
import { LANGUAGES } from '
|
|
116
|
+
import { LANGUAGES } from '../consts/languages.const'
|
|
85
117
|
|
|
86
118
|
const { t } = useT()
|
|
87
119
|
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
/>
|
|
11
11
|
</UTooltip>
|
|
12
12
|
|
|
13
|
-
<UModal
|
|
13
|
+
<UModal
|
|
14
|
+
v-model:open="open"
|
|
15
|
+
:title="t('key.link_key_title', 'Link a key')"
|
|
16
|
+
:ui="{ width: 'sm:max-w-lg' }"
|
|
17
|
+
>
|
|
14
18
|
<template #body>
|
|
15
19
|
<div class="space-y-4">
|
|
16
20
|
<!-- Modifier selector -->
|
|
@@ -40,13 +44,25 @@
|
|
|
40
44
|
|
|
41
45
|
<!-- Key list -->
|
|
42
46
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
43
|
-
<div
|
|
44
|
-
|
|
47
|
+
<div
|
|
48
|
+
v-if="loading"
|
|
49
|
+
class="py-8 text-center"
|
|
50
|
+
>
|
|
51
|
+
<UIcon
|
|
52
|
+
name="i-heroicons-arrow-path"
|
|
53
|
+
class="animate-spin text-gray-400 text-lg"
|
|
54
|
+
/>
|
|
45
55
|
</div>
|
|
46
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
v-else-if="!keys.length"
|
|
58
|
+
class="py-8 text-center text-sm text-gray-400"
|
|
59
|
+
>
|
|
47
60
|
{{ t('key.none_found', 'No key found') }}
|
|
48
61
|
</div>
|
|
49
|
-
<div
|
|
62
|
+
<div
|
|
63
|
+
v-else
|
|
64
|
+
class="max-h-72 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800"
|
|
65
|
+
>
|
|
50
66
|
<button
|
|
51
67
|
v-for="key in keys"
|
|
52
68
|
:key="key.id"
|
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
/>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
<UModal
|
|
17
|
+
<UModal
|
|
18
|
+
v-model:open="open"
|
|
19
|
+
:title="t('pathpicker.title', 'Select a folder')"
|
|
20
|
+
:ui="{ width: 'sm:max-w-xl' }"
|
|
21
|
+
>
|
|
18
22
|
<template #body>
|
|
19
23
|
<div class="space-y-3">
|
|
20
|
-
|
|
21
24
|
<!-- Breadcrumbs + home -->
|
|
22
25
|
<div class="flex items-center gap-1 flex-wrap min-h-6">
|
|
23
26
|
<UButton
|
|
@@ -27,8 +30,14 @@
|
|
|
27
30
|
size="xs"
|
|
28
31
|
@click="browse(data?.home ?? '')"
|
|
29
32
|
/>
|
|
30
|
-
<template
|
|
31
|
-
|
|
33
|
+
<template
|
|
34
|
+
v-for="(crumb, i) in data?.breadcrumbs"
|
|
35
|
+
:key="crumb.path"
|
|
36
|
+
>
|
|
37
|
+
<UIcon
|
|
38
|
+
name="i-heroicons-chevron-right"
|
|
39
|
+
class="text-gray-400 text-xs shrink-0"
|
|
40
|
+
/>
|
|
32
41
|
<button
|
|
33
42
|
class="text-xs px-1.5 py-0.5 rounded transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 font-mono"
|
|
34
43
|
:class="i === (data?.breadcrumbs.length ?? 0) - 1
|
|
@@ -49,51 +58,88 @@
|
|
|
49
58
|
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/60 border-b border-gray-100 dark:border-gray-800 transition-colors"
|
|
50
59
|
@click="browse(data.parent)"
|
|
51
60
|
>
|
|
52
|
-
<UIcon
|
|
61
|
+
<UIcon
|
|
62
|
+
name="i-heroicons-arrow-up"
|
|
63
|
+
class="text-gray-400 shrink-0"
|
|
64
|
+
/>
|
|
53
65
|
<span class="font-mono">../</span>
|
|
54
66
|
</button>
|
|
55
67
|
|
|
56
68
|
<!-- Loading -->
|
|
57
|
-
<div
|
|
58
|
-
|
|
69
|
+
<div
|
|
70
|
+
v-if="loading"
|
|
71
|
+
class="py-8 text-center"
|
|
72
|
+
>
|
|
73
|
+
<UIcon
|
|
74
|
+
name="i-heroicons-arrow-path"
|
|
75
|
+
class="animate-spin text-gray-400 text-lg"
|
|
76
|
+
/>
|
|
59
77
|
</div>
|
|
60
78
|
|
|
61
79
|
<!-- Empty -->
|
|
62
|
-
<div
|
|
80
|
+
<div
|
|
81
|
+
v-else-if="!data?.entries.length"
|
|
82
|
+
class="py-8 text-center text-sm text-gray-400"
|
|
83
|
+
>
|
|
63
84
|
{{ t('pathpicker.empty', 'No subfolder') }}
|
|
64
85
|
</div>
|
|
65
86
|
|
|
66
87
|
<!-- Entries -->
|
|
67
|
-
<div
|
|
88
|
+
<div
|
|
89
|
+
v-else
|
|
90
|
+
class="max-h-72 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800"
|
|
91
|
+
>
|
|
68
92
|
<button
|
|
69
93
|
v-for="entry in data.entries"
|
|
70
94
|
:key="entry.path"
|
|
71
95
|
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800/60 transition-colors group"
|
|
72
96
|
@click="browse(entry.path)"
|
|
73
97
|
>
|
|
74
|
-
<UIcon
|
|
98
|
+
<UIcon
|
|
99
|
+
name="i-heroicons-folder"
|
|
100
|
+
class="text-amber-400 shrink-0"
|
|
101
|
+
/>
|
|
75
102
|
<span class="font-mono text-gray-700 dark:text-gray-300 flex-1 truncate">{{ entry.name }}</span>
|
|
76
|
-
<UIcon
|
|
103
|
+
<UIcon
|
|
104
|
+
name="i-heroicons-chevron-right"
|
|
105
|
+
class="text-gray-300 dark:text-gray-600 shrink-0 group-hover:text-gray-400 transition-colors"
|
|
106
|
+
/>
|
|
77
107
|
</button>
|
|
78
108
|
</div>
|
|
79
109
|
</div>
|
|
80
110
|
|
|
81
111
|
<!-- Current selection -->
|
|
82
112
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2 flex items-center gap-2">
|
|
83
|
-
<UIcon
|
|
113
|
+
<UIcon
|
|
114
|
+
name="i-heroicons-map-pin"
|
|
115
|
+
class="text-primary-500 shrink-0 text-sm"
|
|
116
|
+
/>
|
|
84
117
|
<code class="text-xs font-mono text-gray-700 dark:text-gray-300 flex-1 truncate">{{ data?.current ?? '…' }}</code>
|
|
85
118
|
</div>
|
|
86
119
|
|
|
87
|
-
<p
|
|
120
|
+
<p
|
|
121
|
+
v-if="browseError"
|
|
122
|
+
class="text-xs text-red-500"
|
|
123
|
+
>
|
|
124
|
+
{{ browseError }}
|
|
125
|
+
</p>
|
|
88
126
|
</div>
|
|
89
127
|
</template>
|
|
90
128
|
|
|
91
129
|
<template #footer>
|
|
92
130
|
<div class="flex justify-end gap-2">
|
|
93
|
-
<UButton
|
|
131
|
+
<UButton
|
|
132
|
+
color="neutral"
|
|
133
|
+
variant="ghost"
|
|
134
|
+
@click="open = false"
|
|
135
|
+
>
|
|
94
136
|
{{ t('common.cancel', 'Cancel') }}
|
|
95
137
|
</UButton>
|
|
96
|
-
<UButton
|
|
138
|
+
<UButton
|
|
139
|
+
icon="i-heroicons-check"
|
|
140
|
+
:disabled="!data?.current"
|
|
141
|
+
@click="select"
|
|
142
|
+
>
|
|
97
143
|
{{ t('pathpicker.select', 'Select this folder') }}
|
|
98
144
|
</UButton>
|
|
99
145
|
</div>
|