@kyro-cms/admin 0.12.3 → 0.12.5

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 (110) hide show
  1. package/dist/index.cjs +64 -26
  2. package/dist/index.d.cts +2 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +64 -26
  5. package/package.json +8 -2
  6. package/src/components/ActionBar.tsx +16 -16
  7. package/src/components/ApiHealth.tsx +522 -0
  8. package/src/components/ApiKeysManager.tsx +6 -4
  9. package/src/components/AuditLogsPage.tsx +4 -2
  10. package/src/components/AutoForm.tsx +10 -7
  11. package/src/components/BrandingHub.tsx +3 -1
  12. package/src/components/Dashboard.tsx +44 -37
  13. package/src/components/DashboardMetrics.tsx +168 -58
  14. package/src/components/DetailView.tsx +125 -50
  15. package/src/components/DeveloperCenter.tsx +5 -3
  16. package/src/components/GraphQLPlayground.tsx +10 -8
  17. package/src/components/ListView.tsx +66 -45
  18. package/src/components/LoginPage.tsx +21 -19
  19. package/src/components/MarketplaceManager.tsx +3 -1
  20. package/src/components/MediaGallery.tsx +12 -9
  21. package/src/components/PluginsManager.tsx +6 -4
  22. package/src/components/RestPlayground.tsx +8 -6
  23. package/src/components/SessionsManager.tsx +3 -1
  24. package/src/components/Sidebar.astro +180 -92
  25. package/src/components/UserManagement.tsx +8 -6
  26. package/src/components/UserMenu.tsx +19 -17
  27. package/src/components/VersionHistoryPanel.tsx +6 -4
  28. package/src/components/WebhookManager.tsx +143 -15
  29. package/src/components/autoform/AutoFormApiView.tsx +3 -1
  30. package/src/components/autoform/AutoFormEditView.tsx +5 -3
  31. package/src/components/autoform/AutoFormHeader.tsx +6 -6
  32. package/src/components/blocks/AccordionBlock.tsx +5 -3
  33. package/src/components/blocks/ArrayBlock.tsx +5 -3
  34. package/src/components/blocks/BlockWrapper.tsx +5 -3
  35. package/src/components/blocks/CodeBlock.tsx +4 -2
  36. package/src/components/blocks/FileBlock.tsx +5 -3
  37. package/src/components/blocks/HeroBlock.tsx +5 -3
  38. package/src/components/blocks/ImageBlock.tsx +6 -4
  39. package/src/components/blocks/ListBlock.tsx +5 -3
  40. package/src/components/blocks/ParagraphBlock.tsx +3 -1
  41. package/src/components/blocks/RelationshipBlock.tsx +5 -3
  42. package/src/components/blocks/RichTextBlock.tsx +5 -3
  43. package/src/components/blocks/VideoBlock.tsx +5 -3
  44. package/src/components/fields/AccordionField.tsx +10 -51
  45. package/src/components/fields/ArrayField.tsx +9 -7
  46. package/src/components/fields/ArrayLayout.tsx +3 -1
  47. package/src/components/fields/BlocksField.tsx +24 -75
  48. package/src/components/fields/ButtonField.tsx +3 -1
  49. package/src/components/fields/CardField.tsx +6 -4
  50. package/src/components/fields/GroupLayout.tsx +2 -49
  51. package/src/components/fields/HeadingField.tsx +3 -1
  52. package/src/components/fields/HeadingSubheadingField.tsx +4 -2
  53. package/src/components/fields/HeroField.tsx +8 -6
  54. package/src/components/fields/JSONField.tsx +9 -10
  55. package/src/components/fields/LinkField.tsx +4 -2
  56. package/src/components/fields/ListField.tsx +3 -1
  57. package/src/components/fields/MarkdownField.tsx +3 -17
  58. package/src/components/fields/RelationshipBlockField.tsx +7 -5
  59. package/src/components/fields/RelationshipField.tsx +3 -1
  60. package/src/components/fields/RichTextField.tsx +19 -16
  61. package/src/components/fields/SecretField.tsx +9 -69
  62. package/src/components/fields/SelectField.tsx +11 -6
  63. package/src/components/fields/TabsLayout.tsx +2 -48
  64. package/src/components/fields/TextField.tsx +9 -7
  65. package/src/components/fields/UploadField.tsx +3 -1
  66. package/src/components/fields/VideoField.tsx +6 -4
  67. package/src/components/ui/BlockDrawer.tsx +3 -1
  68. package/src/components/ui/CommandPalette.tsx +9 -7
  69. package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
  70. package/src/components/ui/IconPickerModal.tsx +4 -2
  71. package/src/components/ui/PageHeader.tsx +48 -20
  72. package/src/components/users/UserDetail.tsx +3 -1
  73. package/src/components/users/UserForm.tsx +5 -3
  74. package/src/fields/examples/sample-field-2.tsx +3 -1
  75. package/src/fields/examples/sample-field.tsx +3 -1
  76. package/src/hooks/useAutoFormState.ts +19 -11
  77. package/src/integration.ts +21 -0
  78. package/src/layouts/AdminLayout.astro +252 -189
  79. package/src/layouts/AuthLayout.astro +52 -31
  80. package/src/lib/api.ts +7 -7
  81. package/src/lib/autoform-store.ts +4 -0
  82. package/src/lib/deep-equal.ts +3 -3
  83. package/src/lib/i18n.ts +44 -0
  84. package/src/lib/vite-shim-plugin.ts +3 -0
  85. package/src/locales/de.json +163 -0
  86. package/src/locales/en.json +163 -0
  87. package/src/locales/es.json +163 -0
  88. package/src/locales/fr.json +163 -0
  89. package/src/locales/pt.json +163 -0
  90. package/src/pages/403.astro +2 -1
  91. package/src/pages/[collection]/[id].astro +23 -10
  92. package/src/pages/[collection]/index.astro +48 -34
  93. package/src/pages/audit/index.astro +2 -1
  94. package/src/pages/graphql-explorer.astro +2 -1
  95. package/src/pages/graphql.astro +2 -1
  96. package/src/pages/health.astro +9 -0
  97. package/src/pages/index.astro +6 -5
  98. package/src/pages/keys.astro +2 -1
  99. package/src/pages/marketplace.astro +2 -1
  100. package/src/pages/media.astro +2 -1
  101. package/src/pages/plugins.astro +2 -1
  102. package/src/pages/rest-playground.astro +2 -1
  103. package/src/pages/roles/index.astro +16 -52
  104. package/src/pages/sessions.astro +2 -1
  105. package/src/pages/settings/[slug].astro +45 -30
  106. package/src/pages/users/[id].astro +1 -0
  107. package/src/pages/users/index.astro +2 -1
  108. package/src/pages/users/new.astro +2 -1
  109. package/src/pages/webhooks.astro +2 -1
  110. package/src/pages/index_ALT.astro +0 -3
@@ -11,7 +11,8 @@ import { adminPath as ADMIN_BASE } from "../lib/paths";
11
11
  import { PageHeader } from "./ui/PageHeader";
12
12
  import { Badge } from "./ui/Badge";
13
13
  import { Pagination } from "./ui/Pagination";
14
-
14
+ import { useTranslation } from "react-i18next";
15
+ import "../lib/i18n";
15
16
 
16
17
  import type { CollectionConfig, Field } from "@kyro-cms/core";
17
18
  import { resolveFieldValue } from "../lib/resolve-field-value";
@@ -60,6 +61,7 @@ export function ListView({
60
61
  onEdit: providedOnEdit,
61
62
  config,
62
63
  }: ListViewProps) {
64
+ const { t } = useTranslation();
63
65
  const collectionSlug = providedSlug || collection.slug;
64
66
  const { permissions } = useAuthStore();
65
67
  const canCreate = permissions?.collections?.[collectionSlug]?.create !== false;
@@ -71,7 +73,13 @@ export function ListView({
71
73
  if (providedOnCreate) {
72
74
  providedOnCreate();
73
75
  } else {
74
- window.location.href = `${ADMIN_BASE}/${collectionSlug}/new`;
76
+ const href = `${ADMIN_BASE}/${collectionSlug}/new`;
77
+ // @ts-ignore
78
+ import("astro:transitions/client").then(({ navigate }) => {
79
+ navigate(href);
80
+ }).catch(() => {
81
+ window.location.href = href;
82
+ });
75
83
  }
76
84
  };
77
85
 
@@ -79,7 +87,13 @@ export function ListView({
79
87
  if (providedOnEdit) {
80
88
  providedOnEdit(id);
81
89
  } else {
82
- window.location.href = `${ADMIN_BASE}/${collectionSlug}/${id}`;
90
+ const href = `${ADMIN_BASE}/${collectionSlug}/${id}`;
91
+ // @ts-ignore
92
+ import("astro:transitions/client").then(({ navigate }) => {
93
+ navigate(href);
94
+ }).catch(() => {
95
+ window.location.href = href;
96
+ });
83
97
  }
84
98
  };
85
99
 
@@ -201,17 +215,24 @@ export function ListView({
201
215
 
202
216
  const displayFields = useMemo(
203
217
  () => {
204
- const fields = allFields.filter((f): f is typeof f & { name: string } => !!f.name && visibleColumns.has(f.name));
205
- if (visibleColumns.has("status")) {
206
- fields.push({
207
- name: "status",
208
- type: "select",
209
- label: "Status",
210
- options: [
211
- { value: "draft", label: "Draft" },
212
- { value: "published", label: "Published" },
213
- ],
214
- } as any);
218
+ const fields: (FieldConfig & { name: string })[] = [];
219
+ for (const colName of Array.from(visibleColumns)) {
220
+ if (colName === "status") {
221
+ fields.push({
222
+ name: "status",
223
+ type: "select",
224
+ label: "Status",
225
+ options: [
226
+ { value: "draft", label: "Draft" },
227
+ { value: "published", label: "Published" },
228
+ ],
229
+ } as any);
230
+ } else {
231
+ const field = allFields.find((f) => f.name === colName);
232
+ if (field && field.name) {
233
+ fields.push(field as typeof field & { name: string });
234
+ }
235
+ }
215
236
  }
216
237
  return fields;
217
238
  },
@@ -297,8 +318,8 @@ export function ListView({
297
318
 
298
319
  const handleBulkDelete = () => {
299
320
  confirm({
300
- title: "Delete Documents",
301
- message: `Are you sure you want to delete ${selectedIds.size} document(s)? This cannot be undone.`,
321
+ title: t("listView.deleteBulkTitle", { defaultValue: "Delete Documents" }),
322
+ message: t("listView.deleteBulkMessage", { defaultValue: "Are you sure you want to delete {{count}} document(s)? This cannot be undone.", count: selectedIds.size }),
302
323
  variant: "danger",
303
324
  onConfirm: async () => {
304
325
  try {
@@ -318,8 +339,8 @@ export function ListView({
318
339
 
319
340
  const handleDeleteSingle = (id: string) => {
320
341
  confirm({
321
- title: "Delete Document",
322
- message: "Are you sure you want to delete this document? This cannot be undone.",
342
+ title: t("listView.deleteTitle", { defaultValue: "Delete Document" }),
343
+ message: t("listView.deleteMessage", { defaultValue: "Are you sure you want to delete this document? This cannot be undone." }),
323
344
  variant: "danger",
324
345
  onConfirm: async () => {
325
346
  try {
@@ -348,8 +369,9 @@ export function ListView({
348
369
  </span>
349
370
  ] : undefined}
350
371
  action={canCreate ? {
351
- label: `Create ${collection.singularLabel || collection.label || collectionSlug}`,
352
- onClick: handleCreate,
372
+ label: t("actions.create", { defaultValue: "Create {{item}}", item: collection.singularLabel || collection.label || collectionSlug }),
373
+ href: providedOnCreate ? undefined : `${ADMIN_BASE}/${collectionSlug}/new`,
374
+ onClick: providedOnCreate ? handleCreate : undefined,
353
375
  icon: Plus,
354
376
  } : undefined}
355
377
 
@@ -363,7 +385,7 @@ export function ListView({
363
385
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
364
386
  <input
365
387
  type="text"
366
- placeholder="Search..."
388
+ placeholder={t("listView.searchPlaceholder", { defaultValue: "Search..." })}
367
389
  value={search}
368
390
  onChange={(e) => setSearch(e.target.value)}
369
391
  className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-sm font-medium text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
@@ -381,7 +403,7 @@ export function ListView({
381
403
  }`}
382
404
  >
383
405
  <Filter className="w-4 h-4" />
384
- Filters
406
+ {t("listView.filters", { defaultValue: "Filters" })}
385
407
  {filters.length > 0 && (
386
408
  <span className="ml-1 px-1.5 py-0.5 bg-[var(--kyro-sidebar-text-active)] text-[var(--kyro-sidebar-active)] rounded-full text-xs">
387
409
  {filters.length}
@@ -397,13 +419,13 @@ export function ListView({
397
419
  className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
398
420
  >
399
421
  <Columns3 className="w-4 h-4" />
400
- Columns
422
+ {t("listView.columns", { defaultValue: "Columns" })}
401
423
  </button>
402
424
  {showColumns && (
403
425
  <div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 overflow-hidden">
404
426
  <div className="p-3 border-b border-[var(--kyro-border)]">
405
427
  <span className="text-xs font-bold tracking-wider text-[var(--kyro-text-secondary)]">
406
- Toggle Columns
428
+ {t("listView.toggleColumns", { defaultValue: "Toggle Columns" })}
407
429
  </span>
408
430
  </div>
409
431
  <div className="p-2 max-h-64 overflow-y-auto">
@@ -438,7 +460,7 @@ export function ListView({
438
460
  onClick={clearAll}
439
461
  className="px-4 py-2 rounded-xl font-bold text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all"
440
462
  >
441
- Clear All
463
+ {t("listView.clearAll", { defaultValue: "Clear All" })}
442
464
  </button>
443
465
  )}
444
466
  </div>
@@ -449,7 +471,7 @@ export function ListView({
449
471
  <div className="surface-tile p-4 border-l-4 border-[var(--kyro-sidebar-active)]">
450
472
  <div className="flex items-center justify-between mb-4">
451
473
  <h3 className="font-medium text-[var(--kyro-text-primary)]">
452
- Advanced Filters
474
+ {t("listView.advancedFilters", { defaultValue: "Advanced Filters" })}
453
475
  </h3>
454
476
  <button
455
477
  type="button"
@@ -457,7 +479,7 @@ export function ListView({
457
479
  className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold text-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all"
458
480
  >
459
481
  <Plus className="w-4 h-4" />
460
- Add Filter
482
+ {t("listView.addFilter", { defaultValue: "Add Filter" })}
461
483
  </button>
462
484
  </div>
463
485
  <div className="space-y-3">
@@ -498,7 +520,7 @@ export function ListView({
498
520
  onChange={(e) =>
499
521
  updateFilter(index, { value: e.target.value })
500
522
  }
501
- placeholder="Value..."
523
+ placeholder={t("fields.value", { defaultValue: "Value..." })}
502
524
  className="flex-1 min-w-[150px] px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)]"
503
525
  />
504
526
  <button
@@ -512,7 +534,7 @@ export function ListView({
512
534
  ))}
513
535
  {filters.length === 0 && (
514
536
  <p className="text-sm text-[var(--kyro-text-muted)]">
515
- No filters applied. Click "Add Filter" to create one.
537
+ {t("listView.noFilters", { defaultValue: 'No filters applied. Click "Add Filter" to create one.' })}
516
538
  </p>
517
539
  )}
518
540
  </div>
@@ -523,7 +545,7 @@ export function ListView({
523
545
  {selectedIds.size > 0 && (
524
546
  <div className="surface-tile p-4 flex items-center justify-between border-l-4 border-[var(--kyro-sidebar-active)]">
525
547
  <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
526
- {selectedIds.size} selected
548
+ {t("listView.selectedCount", { defaultValue: "{{count}} selected", count: selectedIds.size })}
527
549
  </span>
528
550
  <div className="flex gap-2">
529
551
  {canDelete && (
@@ -533,7 +555,7 @@ export function ListView({
533
555
  className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg font-bold text-sm hover:bg-red-600 transition-all"
534
556
  >
535
557
  <Trash2 className="w-4 h-4" />
536
- Delete Selected
558
+ {t("listView.deleteSelected", { defaultValue: "Delete Selected" })}
537
559
  </button>
538
560
  )}
539
561
  <button
@@ -541,7 +563,7 @@ export function ListView({
541
563
  onClick={() => setSelectedIds(new Set())}
542
564
  className="px-4 py-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] font-bold text-sm transition-all"
543
565
  >
544
- Cancel
566
+ {t("actions.cancel", { defaultValue: "Cancel" })}
545
567
  </button>
546
568
  </div>
547
569
  </div>
@@ -559,12 +581,12 @@ export function ListView({
559
581
  <Archive className="w-4 h-4" />
560
582
  </div>
561
583
  <p className="font-medium text-[var(--kyro-text-primary)] text-base">
562
- No documents found
584
+ {t("listView.noDocuments", { defaultValue: "No documents found" })}
563
585
  </p>
564
586
  <p className="text-sm text-[var(--kyro-text-secondary)] mt-1">
565
587
  {hasActiveFilters
566
- ? "Try adjusting your filters or search query."
567
- : `Get started by creating your first ${((collection.singularLabel || collection.label || collectionSlug) as string).toLowerCase()}.`}
588
+ ? t("listView.tryAdjustingFilters", { defaultValue: "Try adjusting your filters or search query." })
589
+ : t("listView.getStartedCreate", { defaultValue: "Get started by creating your first {{item}}.", item: ((collection.singularLabel || collection.label || collectionSlug) as string).toLowerCase() })}
568
590
  </p>
569
591
  {!hasActiveFilters && canCreate && (
570
592
  <button
@@ -573,8 +595,7 @@ export function ListView({
573
595
  className="mt-4 kyro-btn kyro-btn-md kyro-btn-primary shadow-md flex items-center gap-2"
574
596
  >
575
597
  <Plus className="w-4 h-4" />
576
- Create{" "}
577
- {String(collection.singularLabel || collection.label || collectionSlug)}
598
+ {t("actions.create", { defaultValue: "Create {{item}}", item: String(collection.singularLabel || collection.label || collectionSlug) })}
578
599
  </button>
579
600
  )}
580
601
  </div>
@@ -609,12 +630,12 @@ export function ListView({
609
630
  </th>
610
631
  ))}
611
632
  {collection.timestamps ? (
612
- <th className="px-4 py-4">Created</th>
633
+ <th className="px-4 py-4">{t("listView.createdCol", { defaultValue: "Created" })}</th>
613
634
  ) : null}
614
635
  {collection.timestamps ? (
615
- <th className="px-4 py-4">Last Modified</th>
636
+ <th className="px-4 py-4">{t("listView.lastModifiedCol", { defaultValue: "Last Modified" })}</th>
616
637
  ) : null}
617
- <th className="px-4 py-4 text-right">Actions</th>
638
+ <th className="px-4 py-4 text-right">{t("listView.actionsCol", { defaultValue: "Actions" })}</th>
618
639
  </tr>
619
640
  </thead>
620
641
  <tbody className="divide-y divide-[var(--kyro-border)]">
@@ -641,7 +662,7 @@ export function ListView({
641
662
  field.type === "select" && rawValue && Array.isArray(field.options)
642
663
  ? field.options.find((o: any) => o.value === rawValue)
643
664
  ?.label || rawValue
644
- : formatCellValue(rawValue, field.type);
665
+ : formatCellValue(rawValue, field.type, t);
645
666
  return (
646
667
  <td
647
668
  key={field.name}
@@ -688,7 +709,7 @@ export function ListView({
688
709
  type="button"
689
710
  onClick={() => handleEdit(doc.id)}
690
711
  className="flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
691
- title={canUpdate ? "Edit" : "View"}
712
+ title={canUpdate ? t("actions.edit", { defaultValue: "Edit" }) : t("actions.view", { defaultValue: "View" })}
692
713
  >
693
714
  <Edit2 className="w-4 h-4" />
694
715
  </button>
@@ -697,7 +718,7 @@ export function ListView({
697
718
  type="button"
698
719
  onClick={() => handleDeleteSingle(doc.id)}
699
720
  className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[var(--kyro-text-muted)] hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors"
700
- title="Delete"
721
+ title={t("actions.delete", { defaultValue: "Delete" })}
701
722
  >
702
723
  <Trash2 className="w-4 h-4" />
703
724
  </button>
@@ -728,9 +749,9 @@ export function ListView({
728
749
  );
729
750
  }
730
751
 
731
- function formatCellValue(value: any, type?: string): string {
752
+ function formatCellValue(value: any, type?: string, t?: any): string {
732
753
  if (value === null || value === undefined) return "—";
733
- if (typeof value === "boolean") return value ? "Yes" : "No";
754
+ if (typeof value === "boolean") return value ? (t ? t("common.yes", { defaultValue: "Yes" }) : "Yes") : (t ? t("common.no", { defaultValue: "No" }) : "No");
734
755
  if (type === "number" || type === "price") return String(value);
735
756
  if (type === "date" || type === "datetime") {
736
757
  return new Date(value).toLocaleDateString("en-US", {
@@ -3,6 +3,7 @@ import { apiGet, apiPost } from "../lib/api";
3
3
  import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
4
4
  import { Toaster } from "./ui/Toaster";
5
5
  import { useToastStore } from "../lib/stores";
6
+ import { useTranslation } from "react-i18next";
6
7
 
7
8
  interface LoginPageProps {
8
9
  onAuth: (token: string, user: Record<string, unknown>) => void;
@@ -12,6 +13,7 @@ interface LoginPageProps {
12
13
  type AuthMode = "login" | "register";
13
14
 
14
15
  export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
16
+ const { t } = useTranslation();
15
17
  const [mode, setMode] = useState<AuthMode>("login");
16
18
  const [email, setEmail] = useState("");
17
19
  const [password, setPassword] = useState("");
@@ -54,11 +56,11 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
54
56
  localStorage.setItem("kyro_user", JSON.stringify(data.user));
55
57
  addToast(
56
58
  "success",
57
- mode === "login" ? "Welcome back!" : "Account created!",
59
+ mode === "login" ? t("toast.welcomeBack", { defaultValue: "Welcome back!" }) : t("toast.accountCreated", { defaultValue: "Account created!" }),
58
60
  );
59
61
  onAuth(data.token, data.user);
60
62
  } catch {
61
- addToast("error", "Connection failed");
63
+ addToast("error", t("toast.connectionFailed", { defaultValue: "Connection failed" }));
62
64
  } finally {
63
65
  setLoading(false);
64
66
  }
@@ -71,23 +73,23 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
71
73
  <div className="kyro-login-header">
72
74
  <h1 className="kyro-login-title">
73
75
  {isFirstUser
74
- ? "Create Admin Account"
76
+ ? t("login.createAdminAccount", { defaultValue: "Create Admin Account" })
75
77
  : mode === "login"
76
- ? "Sign In"
77
- : "Create Account"}
78
+ ? t("login.signIn", { defaultValue: "Sign In" })
79
+ : t("login.createAccount", { defaultValue: "Create Account" })}
78
80
  </h1>
79
81
  <p className="kyro-login-subtitle">
80
82
  {isFirstUser
81
- ? "Set up your admin account to get started"
83
+ ? t("login.setupAdmin", { defaultValue: "Set up your admin account to get started" })
82
84
  : mode === "login"
83
- ? "Enter your credentials to access the admin"
84
- : "Create an account to access the admin"}
85
+ ? t("login.enterCredentials", { defaultValue: "Enter your credentials to access the admin" })
86
+ : t("login.createAccountDesc", { defaultValue: "Create an account to access the admin" })}
85
87
  </p>
86
88
  </div>
87
89
 
88
90
  <form onSubmit={handleSubmit} className="kyro-login-form">
89
91
  <div className="kyro-form-group">
90
- <label htmlFor="email">Email</label>
92
+ <label htmlFor="email">{t("login.email", { defaultValue: "Email" })}</label>
91
93
  <input
92
94
  id="email"
93
95
  type="email"
@@ -100,7 +102,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
100
102
  </div>
101
103
 
102
104
  <div className="kyro-form-group">
103
- <label htmlFor="password">Password</label>
105
+ <label htmlFor="password">{t("login.password", { defaultValue: "Password" })}</label>
104
106
  <input
105
107
  id="password"
106
108
  type="password"
@@ -117,7 +119,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
117
119
 
118
120
  {mode === "register" && (
119
121
  <div className="kyro-form-group">
120
- <label htmlFor="confirmPassword">Confirm Password</label>
122
+ <label htmlFor="confirmPassword">{t("login.confirmPassword", { defaultValue: "Confirm Password" })}</label>
121
123
  <input
122
124
  id="confirmPassword"
123
125
  type="password"
@@ -138,11 +140,11 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
138
140
  >
139
141
  {loading
140
142
  ? mode === "login"
141
- ? "Signing in..."
142
- : "Creating account..."
143
+ ? t("login.signingIn", { defaultValue: "Signing in..." })
144
+ : t("login.creatingAccount", { defaultValue: "Creating account..." })
143
145
  : mode === "login"
144
- ? "Sign In"
145
- : "Create Account"}
146
+ ? t("login.signIn", { defaultValue: "Sign In" })
147
+ : t("login.createAccount", { defaultValue: "Create Account" })}
146
148
  </button>
147
149
  </form>
148
150
 
@@ -151,24 +153,24 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
151
153
  <p>
152
154
  {mode === "login" ? (
153
155
  <>
154
- Don't have an account?{" "}
156
+ {t("login.noAccount", { defaultValue: "Don't have an account?" })}{" "}
155
157
  <button
156
158
  type="button"
157
159
  className="kyro-login-link"
158
160
  onClick={() => setMode("register")}
159
161
  >
160
- Sign up
162
+ {t("login.signUp", { defaultValue: "Sign up" })}
161
163
  </button>
162
164
  </>
163
165
  ) : (
164
166
  <>
165
- Already have an account?{" "}
167
+ {t("login.hasAccount", { defaultValue: "Already have an account?" })}{" "}
166
168
  <button
167
169
  type="button"
168
170
  className="kyro-login-link"
169
171
  onClick={() => setMode("login")}
170
172
  >
171
- Sign in
173
+ {t("login.signInAction", { defaultValue: "Sign in" })}
172
174
  </button>
173
175
  </>
174
176
  )}
@@ -12,6 +12,7 @@ import {
12
12
  } from "./ui/icons";
13
13
  import { useUIStore, toast } from "../lib/stores";
14
14
  import { Badge } from "./ui/Badge";
15
+ import { useTranslation } from "react-i18next";
15
16
 
16
17
  interface Extension {
17
18
  id: string;
@@ -98,6 +99,7 @@ const mockExtensions: Extension[] = [
98
99
  ];
99
100
 
100
101
  export function MarketplaceManager() {
102
+ const { t } = useTranslation();
101
103
  const [extensions, setExtensions] = useState<Extension[]>(mockExtensions);
102
104
  const [searchQuery, setSearchQuery] = useState("");
103
105
  const [activeCategory, setActiveCategory] = useState("All");
@@ -152,7 +154,7 @@ export function MarketplaceManager() {
152
154
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
153
155
  <input
154
156
  type="text"
155
- placeholder="Search marketplace..."
157
+ placeholder={t("fields.searchMarketplace", { defaultValue: "Search marketplace..." })}
156
158
  value={searchQuery}
157
159
  onChange={(e) => setSearchQuery(e.target.value)}
158
160
  className="pl-9 pr-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl text-xs font-bold w-64 focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all"
@@ -34,7 +34,9 @@ import {
34
34
  withCacheBust,
35
35
  apiUpload,
36
36
  } from "../lib/api";
37
+ import { adminPath } from "../lib/paths";
37
38
  import { useAuthStore, useUIStore, toast } from "../lib/stores";
39
+ import { useTranslation } from "react-i18next";
38
40
 
39
41
  interface MediaItem {
40
42
  id: string;
@@ -141,6 +143,7 @@ export function MediaGallery({
141
143
  multiple?: boolean;
142
144
  pickerMode?: boolean;
143
145
  }) {
146
+ const { t } = useTranslation();
144
147
  const { permissions } = useAuthStore();
145
148
  const canUpload = permissions?.media?.create !== false;
146
149
  const canDelete = permissions?.media?.delete !== false;
@@ -490,7 +493,7 @@ export function MediaGallery({
490
493
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
491
494
  <input
492
495
  type="text"
493
- placeholder="Search assets..."
496
+ placeholder={t("fields.searchAssets", { defaultValue: "Search assets..." })}
494
497
  value={search}
495
498
  onChange={(e) => setSearch(e.target.value)}
496
499
  className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
@@ -968,7 +971,7 @@ export function MediaGallery({
968
971
  <SlidePanel
969
972
  open={!!panelItem}
970
973
  onClose={() => setPanelItem(null)}
971
- title="Asset Details"
974
+ title={t("tooltips.assetDetails", { defaultValue: "Asset Details" })}
972
975
  subtitle={panelItem?.filename}
973
976
  >
974
977
  {panelItem && (
@@ -1064,7 +1067,7 @@ export function MediaGallery({
1064
1067
  updateMetadata(panelItem.id, { alt: e.target.value })
1065
1068
  }
1066
1069
  className="w-full px-4 py-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg text-xs font-bold transition-all resize-none min-h-[80px] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
1067
- placeholder="Describe this asset..."
1070
+ placeholder={t("fields.describeThisAsset", { defaultValue: "Describe this asset..." })}
1068
1071
  />
1069
1072
  </div>
1070
1073
  <div>
@@ -1079,7 +1082,7 @@ export function MediaGallery({
1079
1082
  })
1080
1083
  }
1081
1084
  className="w-full px-4 py-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg text-xs font-bold transition-all resize-none min-h-[80px] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
1082
- placeholder="Add a caption..."
1085
+ placeholder={t("fields.addACaption", { defaultValue: "Add a caption..." })}
1083
1086
  />
1084
1087
  </div>
1085
1088
  </div>
@@ -1119,7 +1122,7 @@ export function MediaGallery({
1119
1122
  try {
1120
1123
  await apiDelete(`/api/media/${panelItem.id}`);
1121
1124
  setPanelItem(null);
1122
- loadMedia();
1125
+ loadMedia(1);
1123
1126
  } catch (error) {
1124
1127
  console.error("Delete failed:", error);
1125
1128
  toast.error("Failed to delete asset");
@@ -1213,15 +1216,15 @@ export function MediaGallery({
1213
1216
  open={showNewFolderModal}
1214
1217
  onClose={() => setShowNewFolderModal(false)}
1215
1218
  onSubmit={createFolder}
1216
- title="Create New Folder"
1217
- placeholder="Folder name"
1219
+ title={t("tooltips.createNewFolder", { defaultValue: "Create New Folder" })}
1220
+ placeholder={t("fields.folderName", { defaultValue: "Folder name" })}
1218
1221
  />
1219
1222
  )}
1220
1223
  {!pickerMode && (
1221
1224
  <Modal
1222
1225
  open={showStorageConfigModal}
1223
1226
  onClose={() => setShowStorageConfigModal(false)}
1224
- title="Storage Not Configured"
1227
+ title={t("tooltips.storageNotConfigured", { defaultValue: "Storage Not Configured" })}
1225
1228
  size="md"
1226
1229
  >
1227
1230
  <div className="text-center">
@@ -1235,7 +1238,7 @@ export function MediaGallery({
1235
1238
  </p>
1236
1239
  <div className="flex gap-3">
1237
1240
  <a
1238
- href="/settings/storage-settings"
1241
+ href={`${adminPath}/settings/storage-settings`}
1239
1242
  className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
1240
1243
  >
1241
1244
  Configure Storage
@@ -14,6 +14,7 @@ import {
14
14
  import { Modal, ModalContent, ModalActions } from "./ui/Modal";
15
15
  import { PageHeader } from "./ui/PageHeader";
16
16
  import { Badge } from "./ui/Badge";
17
+ import { useTranslation } from "react-i18next";
17
18
 
18
19
  interface Plugin {
19
20
  id: string;
@@ -31,6 +32,7 @@ interface ToggleError {
31
32
  }
32
33
 
33
34
  export function PluginsManager() {
35
+ const { t } = useTranslation();
34
36
  const [plugins, setPlugins] = useState<Plugin[]>([]);
35
37
  const [loading, setLoading] = useState(true);
36
38
  const [searchQuery, setSearchQuery] = useState("");
@@ -71,7 +73,7 @@ export function PluginsManager() {
71
73
  return (
72
74
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
73
75
  <PageHeader
74
- title="Plugins"
76
+ title={t("tooltips.plugins", { defaultValue: "Plugins" })}
75
77
  description="Extend Kyro CMS with modular features and integrations."
76
78
  icon={Blocks}
77
79
  />
@@ -85,7 +87,7 @@ export function PluginsManager() {
85
87
  return (
86
88
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
87
89
  <PageHeader
88
- title="Plugins"
90
+ title={t("tooltips.plugins", { defaultValue: "Plugins" })}
89
91
  description="Extend Kyro CMS with modular features and integrations."
90
92
  icon={Blocks}
91
93
  actions={[
@@ -164,7 +166,7 @@ export function PluginsManager() {
164
166
  <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-[var(--kyro-text-secondary)] opacity-40" />
165
167
  <input
166
168
  type="text"
167
- placeholder="Search plugins..."
169
+ placeholder={t("fields.searchPlugins", { defaultValue: "Search plugins..." })}
168
170
  value={searchQuery}
169
171
  onChange={(e) => setSearchQuery(e.target.value)}
170
172
  className="w-full pl-10 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl text-xs focus:outline-none focus:border-[var(--kyro-primary)]/50 transition-all"
@@ -247,7 +249,7 @@ export function PluginsManager() {
247
249
  <Modal
248
250
  open={!!showConfigModal}
249
251
  onClose={() => setShowConfigModal(null)}
250
- title="Plugin Configuration"
252
+ title={t("tooltips.pluginConfiguration", { defaultValue: "Plugin Configuration" })}
251
253
  size="lg"
252
254
  >
253
255
  <ModalContent>
@@ -11,6 +11,7 @@ import {
11
11
  X,
12
12
  ChevronRight,
13
13
  } from "./ui/icons";
14
+ import { useTranslation } from "react-i18next";
14
15
 
15
16
  interface EnvVariable {
16
17
  key: string;
@@ -78,6 +79,7 @@ const METHOD_BG: Record<string, string> = {
78
79
  };
79
80
 
80
81
  export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
82
+ const { t } = useTranslation();
81
83
  const [sidebarTab, setSidebarTab] = useState<"collections" | "saved" | "history" | "env">("collections");
82
84
  const [folders, setFolders] = useState<RequestFolder[]>([]);
83
85
  const [history, setHistory] = useState<HistoryItem[]>([]);
@@ -357,7 +359,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
357
359
  <button
358
360
  onClick={() => setShowSidebar(!showSidebar)}
359
361
  className="p-1 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
360
- title="Toggle sidebar"
362
+ title={t("tooltips.toggleSidebar", { defaultValue: "Toggle sidebar" })}
361
363
  >
362
364
  <ChevronRight className={`w-4 h-4 transition-transform ${showSidebar ? "rotate-180" : ""}`} />
363
365
  </button>
@@ -694,10 +696,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
694
696
  </>
695
697
  )}
696
698
  <div className="ml-auto flex items-center gap-1">
697
- <button onClick={handleCopyResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
699
+ <button onClick={handleCopyResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.copyResponse", { defaultValue: "Copy response" })}>
698
700
  {copied ? <Check className="w-3 h-3 text-[var(--kyro-success)]" /> : <Copy className="w-3 h-3" />}
699
701
  </button>
700
- <button onClick={handleDownloadResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
702
+ <button onClick={handleDownloadResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.downloadResponse", { defaultValue: "Download response" })}>
701
703
  <Download className="w-3 h-3" />
702
704
  </button>
703
705
  </div>
@@ -727,7 +729,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
727
729
  </div>
728
730
 
729
731
  {/* Modals */}
730
- <Modal open={showFolderModal} onClose={() => setShowFolderModal(false)} title="Create Folder" size="sm"
732
+ <Modal open={showFolderModal} onClose={() => setShowFolderModal(false)} title={t("tooltips.createFolder", { defaultValue: "Create Folder" })} size="sm"
731
733
  footer={
732
734
  <div className="flex gap-2">
733
735
  <button onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
@@ -736,11 +738,11 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
736
738
  }
737
739
  >
738
740
  <input type="text" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)}
739
- placeholder="Folder name..." className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
741
+ placeholder={t("fields.folderName", { defaultValue: "Folder name..." })} className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
740
742
  />
741
743
  </Modal>
742
744
 
743
- <Modal open={showSaveModal} onClose={() => setShowSaveModal(false)} title="Save Request" size="sm"
745
+ <Modal open={showSaveModal} onClose={() => setShowSaveModal(false)} title={t("tooltips.saveRequest", { defaultValue: "Save Request" })} size="sm"
744
746
  footer={
745
747
  <div className="flex gap-2">
746
748
  <button onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>