i18n-dashboard 0.13.1 → 0.15.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 (109) hide show
  1. package/README.md +18 -2
  2. package/nuxt.config.ts +9 -0
  3. package/package.json +13 -6
  4. package/src/app.vue +5 -1
  5. package/src/assets/locales/en.json +102 -1
  6. package/src/components/GitRepoManager.vue +28 -6
  7. package/src/components/LanguagePicker.vue +43 -11
  8. package/src/components/LinkedKeyPicker.vue +21 -5
  9. package/src/components/PathPicker.vue +61 -15
  10. package/src/components/PluralEditor.vue +40 -12
  11. package/src/components/ScanModal.vue +102 -29
  12. package/src/components/TranslationHistoryModal.vue +42 -10
  13. package/src/components/TranslationRow.vue +86 -21
  14. package/src/components/dashboard/WidgetConfigModal.vue +23 -8
  15. package/src/components/dashboard/WidgetGrid.vue +40 -11
  16. package/src/components/dashboard/WidgetPicker.vue +24 -8
  17. package/src/components/dashboard/widgets/ActivityWidget.vue +54 -15
  18. package/src/components/dashboard/widgets/LanguagesCoverageWidget.vue +54 -15
  19. package/src/components/dashboard/widgets/ProjectsWidget.vue +36 -9
  20. package/src/components/dashboard/widgets/ReviewWidget.vue +66 -18
  21. package/src/components/dashboard/widgets/StatWidget.vue +34 -9
  22. package/src/composables/useAuth.ts +3 -1
  23. package/src/composables/useProfile.ts +1 -1
  24. package/src/composables/useProject.ts +3 -3
  25. package/src/composables/useReview.ts +54 -7
  26. package/src/composables/useWidgetData.ts +1 -1
  27. package/src/consts/commons.const.ts +10 -0
  28. package/src/interfaces/translation-job.interface.ts +1 -1
  29. package/src/layouts/default.vue +218 -46
  30. package/src/middleware/auth.global.ts +30 -9
  31. package/src/pages/admin/logs.vue +272 -0
  32. package/src/pages/admin/security.vue +197 -0
  33. package/src/pages/admin/smtp.vue +382 -0
  34. package/src/pages/forgot-password.vue +128 -0
  35. package/src/pages/index.vue +4 -1
  36. package/src/pages/login.vue +59 -9
  37. package/src/pages/onboarding.vue +409 -77
  38. package/src/pages/projects/[id]/formats/datetime.vue +172 -36
  39. package/src/pages/projects/[id]/formats/modifiers.vue +148 -32
  40. package/src/pages/projects/[id]/formats/number.vue +190 -38
  41. package/src/pages/projects/[id]/index.vue +200 -39
  42. package/src/pages/projects/[id]/languages.vue +289 -67
  43. package/src/pages/projects/[id]/review.vue +189 -64
  44. package/src/pages/projects/[id]/settings.vue +264 -97
  45. package/src/pages/projects/[id]/translations/[keyId].vue +533 -314
  46. package/src/pages/projects/[id]/translations/index.vue +171 -36
  47. package/src/pages/projects/[id]/users.vue +261 -60
  48. package/src/pages/projects/index.vue +409 -105
  49. package/src/pages/reset-password.vue +164 -0
  50. package/src/pages/users/[id]/profile.vue +267 -60
  51. package/src/pages/users/index.vue +237 -62
  52. package/src/server/api/admin/logs.delete.ts +19 -0
  53. package/src/server/api/admin/logs.get.ts +19 -0
  54. package/src/server/api/admin/smtp-test.post.ts +28 -0
  55. package/src/server/api/admin/smtp.get.ts +25 -0
  56. package/src/server/api/admin/smtp.post.ts +37 -0
  57. package/src/server/api/auth/forgot-password.post.ts +49 -0
  58. package/src/server/api/auth/login.post.ts +1 -1
  59. package/src/server/api/auth/logout.post.ts +1 -1
  60. package/src/server/api/auth/me.get.ts +1 -2
  61. package/src/server/api/auth/me.put.ts +1 -1
  62. package/src/server/api/auth/password.put.ts +10 -4
  63. package/src/server/api/auth/refresh.post.ts +1 -1
  64. package/src/server/api/auth/reset-password.post.ts +41 -0
  65. package/src/server/api/auth/status.get.ts +14 -9
  66. package/src/server/api/config.get.ts +1 -1
  67. package/src/server/api/dashboard/layout.get.ts +1 -1
  68. package/src/server/api/dashboard/layout.post.ts +1 -1
  69. package/src/server/api/export.get.ts +1 -1
  70. package/src/server/api/keys/index.get.ts +27 -9
  71. package/src/server/api/languages/index.post.ts +1 -1
  72. package/src/server/api/onboarding.post.ts +1 -1
  73. package/src/server/api/profile.get.ts +1 -1
  74. package/src/server/api/project-snapshot.post.ts +1 -1
  75. package/src/server/api/projects/detect.post.ts +1 -1
  76. package/src/server/api/scan.post.ts +1 -1
  77. package/src/server/api/settings/password-policy.get.ts +7 -0
  78. package/src/server/api/setup.post.ts +10 -4
  79. package/src/server/api/stats/global.get.ts +1 -1
  80. package/src/server/api/translations/batch-translate.post.ts +7 -0
  81. package/src/server/api/translations/bulk-status.post.ts +1 -1
  82. package/src/server/api/translations/index.post.ts +1 -1
  83. package/src/server/api/translations/job/[id].get.ts +1 -1
  84. package/src/server/api/translations/status.post.ts +1 -1
  85. package/src/server/api/translations/translate-all.post.ts +1 -1
  86. package/src/server/api/users/[id]/profile.get.ts +2 -2
  87. package/src/server/api/users/[id]/roles.put.ts +1 -1
  88. package/src/server/api/users/[id].delete.ts +1 -1
  89. package/src/server/api/users/[id].put.ts +1 -1
  90. package/src/server/api/users/index.get.ts +1 -1
  91. package/src/server/api/users/index.post.ts +11 -6
  92. package/src/server/db/index.ts +129 -11
  93. package/src/server/middleware/auth.ts +11 -6
  94. package/src/server/routes/locale/[lang].get.ts +1 -1
  95. package/src/server/tasks/purge-logs.ts +44 -0
  96. package/src/server/utils/auth.util.ts +10 -9
  97. package/src/server/utils/auto-translate.util.ts +1 -1
  98. package/src/server/utils/log.util.ts +21 -0
  99. package/src/server/utils/mailer.util.ts +70 -11
  100. package/src/server/utils/password.util.ts +46 -0
  101. package/src/server/utils/project-config.util.ts +2 -2
  102. package/src/server/utils/scanner.util.ts +3 -3
  103. package/src/server/utils/translation-job.util.ts +3 -3
  104. package/src/services/base.service.ts +11 -1
  105. package/src/services/profile.service.ts +1 -1
  106. package/src/types/auth.type.ts +1 -1
  107. package/i18n-dashboard.config.example.js +0 -40
  108. package/src/server/consts/commons.const.ts +0 -6
  109. /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
- Looking for feedback 👀
90
- Open an issue or discussion!
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
@@ -41,6 +41,11 @@ export default defineNuxtConfig({
41
41
  localesPath: process.env.I18N_LOCALES_PATH || 'src/locales',
42
42
  // Auth — set SESSION_SECRET to a strong random value in production
43
43
  sessionSecret: process.env.SESSION_SECRET || DEFAULT_SECRET,
44
+ // Security — tunable without code changes
45
+ bcryptRounds: process.env.BCRYPT_ROUNDS || '12',
46
+ sessionTtlMinutes: process.env.SESSION_TTL_MINUTES || '15',
47
+ refreshTokenTtlDays: process.env.REFRESH_TOKEN_TTL_DAYS || '7',
48
+ resetTokenTtlHours: process.env.RESET_TOKEN_TTL_HOURS || '1',
44
49
  // Email (SMTP) — optional
45
50
  smtpHost: process.env.SMTP_HOST || '',
46
51
  smtpPort: process.env.SMTP_PORT || '587',
@@ -69,6 +74,10 @@ export default defineNuxtConfig({
69
74
  dir: './src/assets/locales',
70
75
  },
71
76
  ],
77
+ experimental: { tasks: true },
78
+ scheduledTasks: {
79
+ '0 * * * *': ['purge-logs'],
80
+ },
72
81
  // ── Security headers ─────────────────────────────────────────────────────
73
82
  // Applied to every response from the Nitro server.
74
83
  routeRules: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18n-dashboard",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
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
- "cy:open": "cypress open",
20
- "cy:run": "cypress run",
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
- "cypress": "^15.12.0",
87
+ "eslint": "^10.0.3",
88
+ "eslint-plugin-vue": "^10.8.0",
84
89
  "happy-dom": "^20.8.4",
85
- "vitest": "^4.1.0"
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,6 +1,10 @@
1
1
  <template>
2
2
  <UApp>
3
- <NuxtLoadingIndicator color="rgb(var(--ui-primary))" :height="3" :throttle="100" />
3
+ <NuxtLoadingIndicator
4
+ color="rgb(var(--ui-primary))"
5
+ :height="3"
6
+ :throttle="100"
7
+ />
4
8
  <NuxtLayout>
5
9
  <NuxtPage :transition="{ name: 'page', mode: 'out-in' }" />
6
10
  </NuxtLayout>
@@ -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 :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" />
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 :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" />
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 v-model="local.token" type="password" class="w-full" :placeholder="t('projects.git_token_placeholder', 'ghp_...')" @input="emitCurrent" />
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 '~/interfaces/project.interface'
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 v-if="isSelected(lang.code)" name="i-heroicons-check" class="text-primary-500 shrink-0" />
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 v-if="!filteredList.length" class="px-3 py-4 text-sm text-center text-gray-400">
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 name="i-heroicons-plus-circle" class="shrink-0 text-amber-500" />
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 size="xs" color="warning" variant="soft">BCP 47</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 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>
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
- >{{ t('languages.default_badge', 'Default') }}</UBadge>
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 name="i-heroicons-star" class="text-xs" />
95
+ <UIcon
96
+ name="i-heroicons-star"
97
+ class="text-xs"
98
+ />
73
99
  </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" />
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 '~/consts/languages.const'
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 v-model:open="open" :title="t('key.link_key_title', 'Link a key')" :ui="{ width: 'sm:max-w-lg' }">
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 v-if="loading" class="py-8 text-center">
44
- <UIcon name="i-heroicons-arrow-path" class="animate-spin text-gray-400 text-lg" />
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 v-else-if="!keys.length" class="py-8 text-center text-sm text-gray-400">
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 v-else class="max-h-72 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800">
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 v-model:open="open" :title="t('pathpicker.title', 'Select a folder')" :ui="{ width: 'sm:max-w-xl' }">
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 v-for="(crumb, i) in data?.breadcrumbs" :key="crumb.path">
31
- <UIcon name="i-heroicons-chevron-right" class="text-gray-400 text-xs shrink-0" />
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 name="i-heroicons-arrow-up" class="text-gray-400 shrink-0" />
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 v-if="loading" class="py-8 text-center">
58
- <UIcon name="i-heroicons-arrow-path" class="animate-spin text-gray-400 text-lg" />
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 v-else-if="!data?.entries.length" class="py-8 text-center text-sm text-gray-400">
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 v-else class="max-h-72 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800">
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 name="i-heroicons-folder" class="text-amber-400 shrink-0" />
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 name="i-heroicons-chevron-right" class="text-gray-300 dark:text-gray-600 shrink-0 group-hover:text-gray-400 transition-colors" />
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 name="i-heroicons-map-pin" class="text-primary-500 shrink-0 text-sm" />
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 v-if="browseError" class="text-xs text-red-500">{{ browseError }}</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 color="neutral" variant="ghost" @click="open = false">
131
+ <UButton
132
+ color="neutral"
133
+ variant="ghost"
134
+ @click="open = false"
135
+ >
94
136
  {{ t('common.cancel', 'Cancel') }}
95
137
  </UButton>
96
- <UButton icon="i-heroicons-check" :disabled="!data?.current" @click="select">
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>