@saasicat/ui-vue 0.5.0 → 0.6.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 (186) hide show
  1. package/README.md +17 -0
  2. package/dist/{chunk-EH7TOD4H.js → chunk-A3W2N26K.js} +73 -77
  3. package/dist/{chunk-3YP2ZAOD.js → chunk-QZOSDCNP.js} +36 -1
  4. package/dist/chunk-X5SQ5HMB.js +3231 -0
  5. package/dist/client/index.cjs +3279 -76
  6. package/dist/client/index.d.cts +31 -5
  7. package/dist/client/index.d.ts +31 -5
  8. package/dist/client/index.js +23 -5
  9. package/dist/index.cjs +3709 -465
  10. package/dist/index.d.cts +37 -54
  11. package/dist/index.d.ts +37 -54
  12. package/dist/index.js +347 -311
  13. package/dist/messages-7b0P8W75.d.cts +1652 -0
  14. package/dist/messages-7b0P8W75.d.ts +1652 -0
  15. package/dist/quasar/index.cjs +3185 -4
  16. package/dist/quasar/index.d.cts +11 -2
  17. package/dist/quasar/index.d.ts +11 -2
  18. package/dist/quasar/index.js +8 -3
  19. package/dist/testing-e2e/admin-pages-suite.cjs +14 -11
  20. package/dist/testing-e2e/admin-pages-suite.js +14 -11
  21. package/dist/{ui-notify-COUzXEQ4.d.ts → use-super-admin-i18n-B3v3-SWZ.d.ts} +35 -3
  22. package/dist/{ui-notify-D_eAAwRh.d.cts → use-super-admin-i18n-DMktRVEt.d.cts} +35 -3
  23. package/package.json +2 -2
  24. package/src/client/action-registry.ts +4 -4
  25. package/src/client/batch-column-fetcher.ts +4 -4
  26. package/src/client/boot-loader.ts +3 -3
  27. package/src/client/i18n/currency.ts +42 -0
  28. package/src/client/i18n/define.ts +59 -0
  29. package/src/client/i18n/format.ts +12 -0
  30. package/src/client/i18n/index.ts +14 -0
  31. package/src/client/i18n/locale.ts +15 -0
  32. package/src/client/i18n/messages/audit.ts +42 -0
  33. package/src/client/i18n/messages/bundles.ts +459 -0
  34. package/src/client/i18n/messages/common.ts +147 -0
  35. package/src/client/i18n/messages/dashboard.ts +16 -0
  36. package/src/client/i18n/messages/discovery.ts +229 -0
  37. package/src/client/i18n/messages/email.ts +125 -0
  38. package/src/client/i18n/messages/marketing.ts +270 -0
  39. package/src/client/i18n/messages/nav.ts +72 -0
  40. package/src/client/i18n/messages/pilots.ts +192 -0
  41. package/src/client/i18n/messages/plan-detail.ts +266 -0
  42. package/src/client/i18n/messages/plan-editor.ts +177 -0
  43. package/src/client/i18n/messages/plan-versions.ts +251 -0
  44. package/src/client/i18n/messages/plans.ts +503 -0
  45. package/src/client/i18n/messages/promos.ts +202 -0
  46. package/src/client/i18n/messages/shell.ts +145 -0
  47. package/src/client/i18n/messages/tenants.ts +104 -0
  48. package/src/client/i18n/messages/users.ts +78 -0
  49. package/src/client/i18n/messages.ts +84 -0
  50. package/src/client/index.ts +2 -0
  51. package/src/client/manifest-loader.ts +4 -4
  52. package/src/client/nav-builder.ts +44 -51
  53. package/src/client/plan-versions-catalog.ts +42 -30
  54. package/src/components/BundleVersionPublishDialog.vue +35 -24
  55. package/src/components/MarketingPromotionsTab.vue +112 -59
  56. package/src/components/MfaPromptDialog.vue +11 -11
  57. package/src/components/TenantActionConfirmDialog.vue +19 -17
  58. package/src/components/VersionDiffPreview.vue +72 -43
  59. package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
  60. package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
  61. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
  62. package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
  63. package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
  64. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
  65. package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
  66. package/src/components/bundle-editor/bundle-version-status.ts +29 -29
  67. package/src/components/dialogs/PilotCreateDialog.vue +58 -63
  68. package/src/components/dialogs/PilotEditDialog.vue +40 -34
  69. package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
  70. package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
  71. package/src/components/dialogs/types.ts +3 -13
  72. package/src/components/plan/PlanCycleToggle.vue +20 -9
  73. package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
  74. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
  75. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
  76. package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
  77. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
  78. package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
  79. package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
  80. package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
  81. package/src/components/plan-detail/PlanAuditLog.vue +11 -6
  82. package/src/components/plan-detail/PlanDetail.vue +21 -23
  83. package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
  84. package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
  85. package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
  86. package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
  87. package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
  88. package/src/components/plan-list/PlanList.vue +99 -57
  89. package/src/components/plan-matrix/PlanMatrix.vue +85 -38
  90. package/src/components/plan-review/PlanReview.vue +105 -80
  91. package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
  92. package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
  93. package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
  94. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
  95. package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
  96. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
  97. package/src/index.ts +1 -1
  98. package/src/pages-standard/AdminLayout.vue +42 -17
  99. package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
  100. package/src/pages-standard/AuditPage.vue +58 -19
  101. package/src/pages-standard/BundlesPage.vue +29 -18
  102. package/src/pages-standard/DashboardPage.vue +29 -15
  103. package/src/pages-standard/DiscoveryPage.vue +40 -28
  104. package/src/pages-standard/EmailHistoryPage.vue +92 -59
  105. package/src/pages-standard/MarketingCatalogPage.vue +35 -16
  106. package/src/pages-standard/PilotsPage.vue +57 -41
  107. package/src/pages-standard/PlanVersionsPage.vue +7 -3
  108. package/src/pages-standard/PlansPage.vue +89 -57
  109. package/src/pages-standard/PlatformEmailPage.vue +115 -50
  110. package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
  111. package/src/pages-standard/PromoCodesPage.vue +46 -35
  112. package/src/pages-standard/SubscriptionsPage.vue +14 -9
  113. package/src/pages-standard/SuperAdminLoginPage.vue +16 -13
  114. package/src/pages-standard/SuperAdminSetupWizard.vue +51 -47
  115. package/src/pages-standard/TenantDetailPage.vue +50 -36
  116. package/src/pages-standard/TenantsPage.vue +54 -41
  117. package/src/pages-standard/UsersPage.vue +66 -42
  118. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
  119. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
  120. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
  121. package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
  122. package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
  123. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
  124. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
  125. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
  126. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
  127. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
  128. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
  129. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
  130. package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
  131. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
  132. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
  133. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
  134. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
  135. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
  136. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +41 -25
  137. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +45 -26
  138. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +7 -5
  139. package/src/pages-standard/plan-versions/PlanVersionsList.vue +39 -16
  140. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +24 -12
  141. package/src/pages-standard/plan-versions/format.ts +33 -17
  142. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
  143. package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
  144. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
  145. package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
  146. package/src/pages-standard/tenants/format.ts +3 -2
  147. package/src/pages-tenant/MySubscriptionBundlesPage.vue +72 -37
  148. package/src/pages-tenant/PlanChangeWizard.vue +8 -4
  149. package/src/pages-tenant/TenantPlanSection.vue +17 -13
  150. package/src/pages-tenant/default-i18n.ts +217 -1
  151. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +8 -6
  152. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +9 -10
  153. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
  154. package/src/quasar/create-super-admin-app.ts +19 -0
  155. package/src/testing-e2e/admin-pages-suite.ts +14 -11
  156. package/src/vue/project-page-host.ts +5 -3
  157. package/src/vue/use-audit-entries.ts +3 -3
  158. package/src/vue/use-bulk-publish.ts +6 -4
  159. package/src/vue/use-bundle-versions-map.ts +6 -4
  160. package/src/vue/use-bundles.ts +13 -13
  161. package/src/vue/use-catalog-entries.ts +13 -10
  162. package/src/vue/use-discovery.ts +8 -5
  163. package/src/vue/use-entitlement.ts +3 -3
  164. package/src/vue/use-live-plan-versions.ts +4 -4
  165. package/src/vue/use-marketing-projections.ts +9 -6
  166. package/src/vue/use-nav.ts +8 -2
  167. package/src/vue/use-plan-editor.ts +2 -2
  168. package/src/vue/use-plan-versions.ts +3 -3
  169. package/src/vue/use-plans.ts +12 -12
  170. package/src/vue/use-platform-tenant-actions.ts +9 -6
  171. package/src/vue/use-promotions.ts +9 -10
  172. package/src/vue/use-subscription-draft.ts +2 -3
  173. package/src/vue/use-super-admin-context.ts +3 -3
  174. package/src/vue/use-super-admin-i18n.ts +82 -0
  175. package/src/vue/use-tenant-action-flow.ts +11 -6
  176. package/src/vue/use-tenant-manifest.ts +1 -3
  177. package/src/vue/use-tenant-subscription-bundles.ts +4 -4
  178. package/src/vue/use-tenants.ts +3 -3
  179. package/dist/action-registry-CQ2wL3R-.d.cts +0 -85
  180. package/dist/action-registry-CQ2wL3R-.d.ts +0 -85
  181. package/dist/chunk-LVGUSD3T.js +0 -26
  182. package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
  183. package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
  184. package/src/pages-standard/BusinessTypesPage.vue +0 -662
  185. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
  186. package/src/vue/use-business-types.ts +0 -269
@@ -2,8 +2,10 @@
2
2
  <div class="sa-pilots">
3
3
  <header class="sa-page-head">
4
4
  <div>
5
- <h1 class="sa-page-head__title">Pilot-Mandanten</h1>
6
- <p class="sa-page-head__sub">{{ rows.length }} Pilot-Tenants insgesamt.</p>
5
+ <h1 class="sa-page-head__title">{{ msg.title }}</h1>
6
+ <p class="sa-page-head__sub">
7
+ {{ formatMessage(msg.subtitleCount, { count: rows.length }) }}
8
+ </p>
7
9
  </div>
8
10
  <div class="sa-page-head__actions">
9
11
  <slot name="head-actions">
@@ -12,7 +14,7 @@
12
14
  unelevated
13
15
  color="primary"
14
16
  icon="add"
15
- :label="createLabel"
17
+ :label="resolvedCreateLabel"
16
18
  @click="showCreate = true"
17
19
  />
18
20
  </slot>
@@ -38,7 +40,7 @@
38
40
 
39
41
  <q-banner v-if="reviewSoon.length" class="bg-amber-2 text-grey-9 q-mb-md" rounded>
40
42
  <template #avatar><q-icon name="event" color="amber-9" /></template>
41
- {{ reviewSoon.length }} Pilot-Mandanten enden in den nächsten 30 Tagen — bitte prüfen.
43
+ {{ formatMessage(msg.list.reviewSoonBanner, { count: reviewSoon.length }) }}
42
44
  </q-banner>
43
45
 
44
46
  <div class="sa-pilots__card">
@@ -109,6 +111,8 @@
109
111
  <script setup lang="ts">
110
112
  import { computed, onMounted, ref } from 'vue';
111
113
  import { useQuasar } from 'quasar';
114
+ import { formatMessage } from '../client/i18n/format.js';
115
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
112
116
  import { useSuperAdminNotify } from '../quasar/notify.js';
113
117
  import PilotCreateDialog from '../components/dialogs/PilotCreateDialog.vue';
114
118
  import PilotEditDialog from '../components/dialogs/PilotEditDialog.vue';
@@ -195,11 +199,15 @@ const props = withDefaults(
195
199
  requireMfa: false,
196
200
  requireMfaForExtend: false,
197
201
  requireMfaForRevoke: false,
198
- createLabel: 'Pilot anlegen',
199
202
  defaultActions: () => ['edit', 'extend', 'revoke'],
200
203
  },
201
204
  );
202
205
 
206
+ const msg = useSaMessages('pilots');
207
+ const common = useSaMessages('common');
208
+ const { intlLocale } = useSuperAdminI18n();
209
+ const resolvedCreateLabel = computed(() => props.createLabel ?? msg.value.createAction);
210
+
203
211
  const q = useQuasar();
204
212
  const notify = useSuperAdminNotify();
205
213
  const rows = ref<PilotRow[]>([]);
@@ -291,53 +299,53 @@ const statTiles = computed<
291
299
  const counts = { all: rows.value.length, active: 0, expiring: 0, expired: 0 };
292
300
  for (const r of rows.value) counts[classifyRow(r)]++;
293
301
  return [
294
- { id: 'all', label: 'Alle', count: counts.all },
295
- { id: 'active', label: 'Aktiv', count: counts.active, tone: 'positive' },
302
+ { id: 'all', label: common.value.all, count: counts.all },
303
+ { id: 'active', label: common.value.active, count: counts.active, tone: 'positive' },
296
304
  {
297
305
  id: 'expiring',
298
- label: 'Läuft bald aus',
306
+ label: msg.value.list.tileExpiring,
299
307
  count: counts.expiring,
300
308
  tone: 'warn',
301
- hint: '≤ 14 Tage',
309
+ hint: msg.value.list.tileExpiringHint,
302
310
  },
303
- { id: 'expired', label: 'Abgelaufen', count: counts.expired, tone: 'danger' },
311
+ { id: 'expired', label: common.value.expired, count: counts.expired, tone: 'danger' },
304
312
  ];
305
313
  });
306
314
 
307
- const baseColumns = [
315
+ const baseColumns = computed(() => [
308
316
  {
309
317
  name: 'slug',
310
- label: 'Slug',
318
+ label: msg.value.list.columnSlug,
311
319
  field: (r: PilotRow) => r.tenant.slug,
312
320
  align: 'left' as const,
313
321
  sortable: true,
314
322
  },
315
323
  {
316
324
  name: 'name',
317
- label: 'Name',
325
+ label: common.value.name,
318
326
  field: (r: PilotRow) => r.tenant.name,
319
327
  align: 'left' as const,
320
328
  },
321
- { name: 'plan', label: 'Plan', field: 'plan', align: 'left' as const },
329
+ { name: 'plan', label: msg.value.list.columnPlan, field: 'plan', align: 'left' as const },
322
330
  {
323
331
  name: 'note',
324
- label: 'Note',
332
+ label: msg.value.list.columnNote,
325
333
  field: (r: PilotRow) => r.pilotNote ?? '—',
326
334
  align: 'left' as const,
327
335
  },
328
336
  {
329
337
  name: 'grantedBy',
330
- label: 'Granted by',
338
+ label: msg.value.list.columnGrantedBy,
331
339
  field: (r: PilotRow) => r.grantedBy ?? '—',
332
340
  align: 'left' as const,
333
341
  },
334
342
  {
335
343
  name: 'pilotEndsAt',
336
- label: 'Endet',
344
+ label: msg.value.list.columnEndsAt,
337
345
  field: (r: PilotRow) => formatDate(r.pilotEndsAt) ?? '∞',
338
346
  align: 'left' as const,
339
347
  },
340
- ];
348
+ ]);
341
349
 
342
350
  // Built-in default actions (edit/extend/revoke) — are APPENDED to the
343
351
  // consumer `actions`, not replaced. Order follows the `defaultActions` prop.
@@ -347,7 +355,7 @@ const bakedActions = computed<PilotRowAction[]>(() => {
347
355
  if (id === 'edit' && props.enableEdit && props.submitEdit) {
348
356
  out.push({
349
357
  id: 'edit',
350
- label: 'Bearbeiten',
358
+ label: common.value.edit,
351
359
  icon: 'edit',
352
360
  color: 'primary',
353
361
  handler: (row) => onEditClick(row),
@@ -355,7 +363,7 @@ const bakedActions = computed<PilotRowAction[]>(() => {
355
363
  } else if (id === 'extend' && props.enableExtend && props.submitExtend) {
356
364
  out.push({
357
365
  id: 'extend',
358
- label: 'Verlängern',
366
+ label: msg.value.list.actionExtend,
359
367
  icon: 'event_repeat',
360
368
  color: 'primary',
361
369
  handler: (row) => onExtendClick(row),
@@ -363,7 +371,7 @@ const bakedActions = computed<PilotRowAction[]>(() => {
363
371
  } else if (id === 'revoke' && props.enableRevoke && props.submitRevoke) {
364
372
  out.push({
365
373
  id: 'revoke',
366
- label: 'Widerrufen',
374
+ label: msg.value.list.actionRevoke,
367
375
  icon: 'block',
368
376
  color: 'negative',
369
377
  handler: (row) => onRevokeClick(row),
@@ -379,7 +387,7 @@ const mergedActions = computed<readonly PilotRowAction[]>(() => [
379
387
  ]);
380
388
 
381
389
  const effectiveColumns = computed(() => {
382
- const cols = [...baseColumns];
390
+ const cols = [...baseColumns.value];
383
391
  if (mergedActions.value.length > 0) {
384
392
  cols.push({
385
393
  name: 'actions',
@@ -437,7 +445,7 @@ function errMsg(err: unknown): string {
437
445
  return (
438
446
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
439
447
  (err as Error)?.message ??
440
- 'Aktion fehlgeschlagen'
448
+ msg.value.list.actionFailed
441
449
  );
442
450
  }
443
451
 
@@ -447,15 +455,23 @@ function onEditClick(row: PilotRow): void {
447
455
  }
448
456
 
449
457
  function onUpdated(result: PilotEditResult): void {
450
- notify('positive', `Pilot ${result.slug} aktualisiert.`, {
451
- caption: result.changed?.length ? `Geändert: ${result.changed.join(', ')}` : undefined,
458
+ notify('positive', formatMessage(msg.value.list.updatedNotice, { slug: result.slug }), {
459
+ caption: result.changed?.length
460
+ ? formatMessage(msg.value.list.updatedChangedCaption, {
461
+ fields: result.changed.join(', '),
462
+ })
463
+ : undefined,
452
464
  });
453
465
  void reload();
454
466
  }
455
467
 
456
468
  function onCreated(result: PilotCreateResult): void {
457
- notify('positive', `Pilot ${result.slug} angelegt.`, {
458
- caption: result.initialPassword ? `Initial-Passwort: ${result.initialPassword}` : undefined,
469
+ notify('positive', formatMessage(msg.value.list.createdNotice, { slug: result.slug }), {
470
+ caption: result.initialPassword
471
+ ? formatMessage(msg.value.list.createdPasswordCaption, {
472
+ password: result.initialPassword,
473
+ })
474
+ : undefined,
459
475
  timeoutMs: 8000,
460
476
  });
461
477
  void reload();
@@ -493,7 +509,7 @@ async function runAction(
493
509
  } catch (err) {
494
510
  const status = (err as { response?: { status?: number } })?.response?.status;
495
511
  if (status === 401) {
496
- mfaError.value = 'TOTP-Code ungültig oder MFA nicht eingerichtet.';
512
+ mfaError.value = msg.value.mfa.invalidOrNotSetUp;
497
513
  continue;
498
514
  }
499
515
  showMfa.value = false;
@@ -507,19 +523,19 @@ function onExtendClick(row: PilotRow): void {
507
523
  if (!props.submitExtend) return;
508
524
  const submit = props.submitExtend;
509
525
  q.dialog({
510
- title: `Pilot ${row.tenant.slug} verlängern`,
511
- message: 'Neues Enddatum:',
526
+ title: formatMessage(msg.value.list.extendTitle, { slug: row.tenant.slug }),
527
+ message: msg.value.list.extendMessage,
512
528
  prompt: {
513
529
  model: row.pilotEndsAt?.slice(0, 10) ?? '',
514
530
  type: 'date',
515
531
  },
516
- cancel: 'Abbrechen',
517
- ok: { label: 'Verlängern', color: 'primary' },
532
+ cancel: common.value.cancel,
533
+ ok: { label: msg.value.list.actionExtend, color: 'primary' },
518
534
  }).onOk(async (until: string) => {
519
535
  if (!until) return;
520
536
  await runAction(
521
- `Pilot "${row.tenant.slug}" bis ${until} verlängern.`,
522
- `Pilot bis ${until} verlängert.`,
537
+ formatMessage(msg.value.list.extendMfaDescription, { slug: row.tenant.slug, until }),
538
+ formatMessage(msg.value.list.extendSuccess, { until }),
523
539
  !!props.requireMfaForExtend,
524
540
  (code) => submit(row.tenant.slug, until, code),
525
541
  );
@@ -530,14 +546,14 @@ function onRevokeClick(row: PilotRow): void {
530
546
  if (!props.submitRevoke) return;
531
547
  const submit = props.submitRevoke;
532
548
  q.dialog({
533
- title: `Pilot ${row.tenant.slug} widerrufen`,
534
- message: 'Pilot-Status entfernen. Subscription bleibt bestehen.',
535
- cancel: 'Abbrechen',
536
- ok: { label: 'Widerrufen', color: 'negative' },
549
+ title: formatMessage(msg.value.list.revokeTitle, { slug: row.tenant.slug }),
550
+ message: msg.value.list.revokeMessage,
551
+ cancel: common.value.cancel,
552
+ ok: { label: msg.value.list.actionRevoke, color: 'negative' },
537
553
  }).onOk(async () => {
538
554
  await runAction(
539
- `Pilot-Status für "${row.tenant.slug}" widerrufen.`,
540
- 'Pilot widerrufen.',
555
+ formatMessage(msg.value.list.revokeMfaDescription, { slug: row.tenant.slug }),
556
+ msg.value.list.revokeSuccess,
541
557
  !!props.requireMfaForRevoke,
542
558
  (code) => submit(row.tenant.slug, code),
543
559
  );
@@ -547,7 +563,7 @@ function onRevokeClick(row: PilotRow): void {
547
563
  function formatDate(iso: string | null | undefined): string | null {
548
564
  if (!iso) return null;
549
565
  try {
550
- return new Date(iso).toLocaleDateString('de-DE', {
566
+ return new Date(iso).toLocaleDateString(intlLocale.value, {
551
567
  day: '2-digit',
552
568
  month: 'short',
553
569
  year: 'numeric',
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="sa-pv">
3
3
  <div v-if="loading" class="sa-pv__loading">
4
- <q-spinner size="32px" /> Catalog wird geladen…
4
+ <q-spinner size="32px" /> {{ msg.page.loadingCatalog }}
5
5
  </div>
6
6
 
7
7
  <template v-else>
@@ -63,7 +63,7 @@
63
63
  :action-meta="auditActionMeta"
64
64
  />
65
65
  <div v-else class="sa-pv__no-audit">
66
- Kein Audit-Loader konfiguriert.
66
+ {{ msg.page.noAuditLoader }}
67
67
  </div>
68
68
  </slot>
69
69
  </template>
@@ -85,6 +85,7 @@ import PlanVersionsMatrix from './plan-versions/PlanVersionsMatrix.vue';
85
85
  import PlanVersionsAudit from './plan-versions/PlanVersionsAudit.vue';
86
86
  import PlanVersionsDiff from './plan-versions/PlanVersionsDiff.vue';
87
87
  import type { PlanVersionViewMode } from './plan-versions/types.js';
88
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
88
89
 
89
90
  // Platform standard page: plan versions.
90
91
  //
@@ -153,12 +154,15 @@ const emit = defineEmits<{
153
154
  (e: 'createPlanDraft', planId: string): void;
154
155
  }>();
155
156
 
157
+ const msg = useSaMessages('planVersions');
158
+ const { locale } = useSuperAdminI18n();
159
+
156
160
  const selectedId = ref<string>('drafts');
157
161
  const compareId = ref<string | null>(null);
158
162
  const viewMode = ref<PlanVersionViewMode>('list');
159
163
 
160
164
  const snapshots = computed<CatalogSnapshot[]>(() =>
161
- buildSnapshots(props.data, { planSortOrder: props.planSortOrder }),
165
+ buildSnapshots(props.data, { planSortOrder: props.planSortOrder, locale: locale.value }),
162
166
  );
163
167
 
164
168
  const selected = computed<CatalogSnapshot | null>(
@@ -6,13 +6,13 @@
6
6
  </template>
7
7
  {{ error.message }}
8
8
  <template #action>
9
- <q-btn flat label="Erneut laden" @click="load" />
9
+ <q-btn flat :label="common.reload" @click="load" />
10
10
  </template>
11
11
  </q-banner>
12
12
 
13
13
  <div v-if="loading && plans.length === 0" class="sa-plans__loading">
14
14
  <q-spinner size="32px" />
15
- <span>Pläne werden geladen…</span>
15
+ <span>{{ msg.page.loading }}</span>
16
16
  </div>
17
17
 
18
18
  <template v-else>
@@ -179,6 +179,9 @@ import {
179
179
  type UsePlanVersionsResult,
180
180
  } from '../vue/use-plans.js';
181
181
  import type { HttpClient } from '../client/types.js';
182
+ import type { SaMessages } from '../client/i18n/index.js';
183
+ import { formatMessage } from '../client/i18n/format.js';
184
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
182
185
  import PlanVersionEditor from '../components/plan-version-editor/PlanVersionEditor.vue';
183
186
  import PlanMatrix from '../components/plan-matrix/PlanMatrix.vue';
184
187
  import PlanDetail from '../components/plan-detail/PlanDetail.vue';
@@ -247,6 +250,9 @@ const props = defineProps<{
247
250
  loadPlanAudit?: (planId: string) => Promise<AuditRow[]>;
248
251
  }>();
249
252
 
253
+ const msg = useSaMessages('plans');
254
+ const common = useSaMessages('common');
255
+
250
256
  const composable: UsePlansResult = usePlans({
251
257
  adminEndpoint: props.adminEndpoint,
252
258
  projectKey: props.projectKey,
@@ -390,7 +396,7 @@ async function onCreateSubmit(payload: PlanCreateSubmit): Promise<void> {
390
396
 
391
397
  await reloadAllVersions();
392
398
  flashHighlight(created.planKey);
393
- flashToast(`Plan ${created.planKey} angelegt — jetzt die erste Version zusammenstellen.`);
399
+ flashToast(formatMessage(msg.value.page.toastPlanCreated, { planKey: created.planKey }));
394
400
  await onOpenPlan(created);
395
401
  openCreateDraftWithPrefill({
396
402
  features: payload.initialFeatures,
@@ -550,7 +556,7 @@ async function onUpdatePlanFromDetail(patch: { label: string }): Promise<void> {
550
556
  selectedPlan.value = updated;
551
557
  } catch (err: unknown) {
552
558
  // eslint-disable-next-line no-console
553
- console.error('[PlansPage] Plan-Rename fehlgeschlagen', err);
559
+ console.error('[PlansPage] Plan rename failed', err);
554
560
  }
555
561
  }
556
562
 
@@ -564,7 +570,10 @@ const availableBundles = ref<BundleEntry[]>([]);
564
570
  // all consumer admin UIs label things identically.
565
571
  const featureCatalogEntries = ref<FeatureCatalogEntryRow[]>([]);
566
572
  const featureRegistry = computed<Record<string, { label?: string; group?: string }>>(() => ({
567
- ...buildFeatureRegistry(featureCatalogEntries.value, props.displayLocale ?? CATALOG_DEFAULT_LOCALE),
573
+ ...buildFeatureRegistry(
574
+ featureCatalogEntries.value,
575
+ props.displayLocale ?? CATALOG_DEFAULT_LOCALE,
576
+ ),
568
577
  ...props.featureRegistry,
569
578
  }));
570
579
 
@@ -659,28 +668,32 @@ function describeDraftSaveError(err: unknown): string {
659
668
  const body = e?.body;
660
669
  const code = body?.code;
661
670
  if (status === 401 || status === 403) {
662
- return 'Sitzung abgelaufen — bitte neu anmelden und erneut speichern.';
671
+ return msg.value.page.errorSessionExpiredSave;
663
672
  }
664
673
  if (status === 422 && code === 'STRICT_MODE_VIOLATIONS') {
665
674
  const list = (body?.warnings ?? [])
666
675
  .map((w) => w.message)
667
676
  .filter(Boolean)
668
677
  .join(' · ');
669
- return `Strict-Mode-Check: ${list || 'Drift gegen den Discovery-Snapshot.'}`;
678
+ return formatMessage(msg.value.page.errorStrictMode, {
679
+ details: list || msg.value.page.errorStrictModeFallback,
680
+ });
670
681
  }
671
682
  if (status === 422 && code === 'PLAN_VERSION_REGRESSION') {
672
- return 'Diese Version ist regressiv (Feature entfernt / Quota gesenkt / Preis erhöht). Publish erfordert Force-Regressive — beim Speichern als Draft sollte das nicht auftreten.';
683
+ return msg.value.page.errorDraftRegression;
673
684
  }
674
685
  if (status === 422 && body?.message) {
675
686
  // e.g. "Plan 'BASIC' hat bereits eine Draft-Version v4 …"
676
687
  return body.message;
677
688
  }
678
689
  if (status !== undefined) {
679
- return body?.message ?? `Speichern fehlgeschlagen (HTTP ${status}).`;
690
+ return (
691
+ body?.message ?? formatMessage(msg.value.page.errorSaveFailedHttp, { status: status })
692
+ );
680
693
  }
681
694
  return err instanceof Error
682
- ? `Speichern fehlgeschlagen: ${err.message}`
683
- : 'Speichern fehlgeschlagen — Details siehe Browser-Konsole.';
695
+ ? formatMessage(msg.value.page.errorSaveFailedDetail, { message: err.message })
696
+ : msg.value.page.errorSaveFailedConsole;
684
697
  }
685
698
  const draftEditing = ref<{
686
699
  editingId: string | null;
@@ -903,13 +916,18 @@ async function onReviewSaveExit(): Promise<void> {
903
916
  if (!saved) return;
904
917
  const planKey = selectedPlan.value?.planKey ?? '';
905
918
  flashHighlight(planKey);
906
- flashToast(`Draft v${saved.version} von ${planKey} gespeichert.`);
919
+ flashToast(
920
+ formatMessage(msg.value.page.toastDraftSaved, {
921
+ version: saved.version,
922
+ planKey,
923
+ }),
924
+ );
907
925
  reviewDraft.value = null;
908
926
  draftEditing.value = null;
909
927
  mode.value = selectedPlan.value ? 'cockpit' : 'list';
910
928
  } catch (err: unknown) {
911
929
  // eslint-disable-next-line no-console
912
- console.error('[PlansPage] Draft speichern fehlgeschlagen', err);
930
+ console.error('[PlansPage] Saving the draft failed', err);
913
931
  reviewError.value = describeDraftSaveError(err);
914
932
  } finally {
915
933
  draftSaving.value = false;
@@ -936,14 +954,17 @@ async function onReviewPublish(payload: {
936
954
  const planKey = selectedPlan.value.planKey;
937
955
  flashHighlight(planKey);
938
956
  flashToast(
939
- `Plan ${planKey} wurde als v${result.planVersion.version} veröffentlicht — sichtbar im Katalog.`,
957
+ formatMessage(msg.value.page.toastPlanPublished, {
958
+ planKey,
959
+ version: result.planVersion.version,
960
+ }),
940
961
  );
941
962
  reviewDraft.value = null;
942
963
  draftEditing.value = null;
943
964
  mode.value = 'cockpit';
944
965
  } catch (err: unknown) {
945
966
  // eslint-disable-next-line no-console
946
- console.error('[PlansPage] Publish fehlgeschlagen', err);
967
+ console.error('[PlansPage] Publishing failed', err);
947
968
  reviewError.value = describePublishError(err);
948
969
  } finally {
949
970
  publishing.value = false;
@@ -974,31 +995,29 @@ async function executeArchive(): Promise<void> {
974
995
  await hardDelete(plan.id);
975
996
  await reloadAllVersions();
976
997
  archiveOpen.value = false;
977
- flashToast(`Plan ${plan.planKey} komplett aus der DB gelöscht.`);
998
+ flashToast(formatMessage(msg.value.page.toastPlanDeleted, { planKey: plan.planKey }));
978
999
  } catch (err: unknown) {
979
1000
  // eslint-disable-next-line no-console
980
- console.error('[PlansPage] Archive/Purge fehlgeschlagen', err);
1001
+ console.error('[PlansPage] Archive/purge failed', err);
981
1002
  const status = (err as { status?: number })?.status;
982
1003
  const body = (err as { body?: { code?: string; message?: string } })?.body;
983
1004
  const errMessage = err instanceof Error ? err.message : String(err);
984
1005
  if (status === 422 && body?.code === 'PLAN_HAS_PUBLISHED_VERSIONS') {
985
- archiveError.value =
986
- body.message ??
987
- 'Plan hat published Versionen — kann nicht gelöscht werden (Vertragsschutz P1).';
1006
+ archiveError.value = body.message ?? msg.value.page.errorPlanHasPublishedVersions;
988
1007
  } else if (status === 422 && body?.code === 'PLAN_HAS_DRAFTS') {
989
- archiveError.value =
990
- body.message ??
991
- 'Plan hat noch einen offenen Draft — bitte erst über das Mülleimer-Icon verwerfen.';
1008
+ archiveError.value = body.message ?? msg.value.page.errorPlanHasDrafts;
992
1009
  } else if (status === 422 && body?.code === 'PLAN_HARD_DELETE_NOT_IMPLEMENTED') {
993
- archiveError.value =
994
- 'Backend unterstützt Hard-Delete noch nicht — API-Server neu bauen + starten.';
1010
+ archiveError.value = msg.value.page.errorHardDeleteNotImplemented;
995
1011
  } else if (status === 404) {
996
- archiveError.value = 'Plan wurde bereits entfernt (404). Liste wird neu geladen.';
1012
+ archiveError.value = msg.value.page.errorPlanAlreadyRemoved;
997
1013
  await reloadAllVersions();
998
1014
  } else if (status !== undefined) {
999
- archiveError.value = body?.message ?? `Vorgang fehlgeschlagen (HTTP ${status}).`;
1015
+ archiveError.value =
1016
+ body?.message ?? formatMessage(msg.value.page.errorOperationFailedHttp, { status });
1000
1017
  } else {
1001
- archiveError.value = `Vorgang fehlgeschlagen: ${errMessage}. Details siehe Browser-Konsole.`;
1018
+ archiveError.value = formatMessage(msg.value.page.errorOperationFailedDetail, {
1019
+ message: errMessage,
1020
+ });
1002
1021
  }
1003
1022
  } finally {
1004
1023
  archiving.value = false;
@@ -1037,35 +1056,38 @@ async function executeDiscard(): Promise<void> {
1037
1056
  await reloadAllVersions();
1038
1057
  discardOpen.value = false;
1039
1058
  flashToast(
1040
- `Draft v${draft.version} von ${plan.planKey} verworfen — Live-Version unverändert.`,
1059
+ formatMessage(msg.value.page.toastDraftDiscarded, {
1060
+ version: draft.version,
1061
+ planKey: plan.planKey,
1062
+ }),
1041
1063
  );
1042
1064
  } catch (err: unknown) {
1043
1065
  // Full error object to the console — in the diagnostic case you can see
1044
1066
  // e.g. CORS preflight errors or the real network stack there.
1045
1067
  // eslint-disable-next-line no-console
1046
- console.error('[PlansPage] Discard fehlgeschlagen', err);
1068
+ console.error('[PlansPage] Discard failed', err);
1047
1069
 
1048
1070
  const status = (err as { status?: number })?.status;
1049
1071
  const body = (err as { body?: { code?: string; message?: string } })?.body;
1050
1072
  const errMessage = err instanceof Error ? err.message : String(err);
1051
1073
 
1052
1074
  if (status === 422 && body?.code === 'PLAN_VERSION_ALREADY_PUBLISHED') {
1053
- discardError.value =
1054
- 'Diese Version ist bereits published und kann nicht verworfen werden.';
1075
+ discardError.value = msg.value.page.errorVersionAlreadyPublished;
1055
1076
  } else if (status === 422 && body?.code === 'PLAN_VERSION_DISCARD_NOT_IMPLEMENTED') {
1056
- discardError.value =
1057
- 'Backend unterstützt das Verwerfen noch nicht — bitte API-Server neu bauen + starten.';
1077
+ discardError.value = msg.value.page.errorDiscardNotImplemented;
1058
1078
  } else if (status === 404) {
1059
- discardError.value =
1060
- 'Draft wurde bereits entfernt (404) — Liste wird gleich neu geladen.';
1079
+ discardError.value = msg.value.page.errorDraftAlreadyRemoved;
1061
1080
  await reloadAllVersions();
1062
1081
  } else if (status !== undefined) {
1063
- discardError.value = body?.message ?? `Verwerfen fehlgeschlagen (HTTP ${status}).`;
1082
+ discardError.value =
1083
+ body?.message ?? formatMessage(msg.value.page.errorDiscardFailedHttp, { status });
1064
1084
  } else {
1065
1085
  // No HTTP status → usually a network error / CORS preflight / failed
1066
1086
  // caller-side (e.g. usePlanVersions construction). Show the real
1067
1087
  // error message so the diagnostic path doesn't end at "HTTP ?".
1068
- discardError.value = `Verwerfen fehlgeschlagen: ${errMessage}. Details siehe Browser-Konsole (oft: API-Server alt gebaut oder CORS blockiert DELETE).`;
1088
+ discardError.value = formatMessage(msg.value.page.errorDiscardFailedDetail, {
1089
+ message: errMessage,
1090
+ });
1069
1091
  }
1070
1092
  } finally {
1071
1093
  discarding.value = false;
@@ -1078,13 +1100,15 @@ async function executeDiscard(): Promise<void> {
1078
1100
  // consistent after the call.
1079
1101
  async function onSubmitTerminate(versionId: string, endsAt: string): Promise<void> {
1080
1102
  if (!planVersions.value) {
1081
- throw new Error('PlanVersions-Composable nicht initialisiert');
1103
+ throw new Error('PlanVersions composable is not initialized');
1082
1104
  }
1083
1105
  await planVersions.value.terminateVersion(versionId, endsAt);
1084
1106
  await reloadCockpitVersions();
1085
1107
  if (selectedPlan.value) {
1086
1108
  flashToast(
1087
- `v${versions.value.find((v) => v.id === versionId)?.version ?? '?'} terminiert.`,
1109
+ formatMessage(msg.value.page.toastVersionTerminated, {
1110
+ version: versions.value.find((v) => v.id === versionId)?.version ?? '?',
1111
+ }),
1088
1112
  );
1089
1113
  }
1090
1114
  }
@@ -1122,7 +1146,10 @@ async function executePublish(): Promise<void> {
1122
1146
  if (selectedPlan.value) {
1123
1147
  flashHighlight(selectedPlan.value.planKey);
1124
1148
  flashToast(
1125
- `Plan ${selectedPlan.value.planKey} wurde als v${result.planVersion.version} veröffentlicht — sichtbar im Katalog.`,
1149
+ formatMessage(msg.value.page.toastPlanPublished, {
1150
+ planKey: selectedPlan.value.planKey,
1151
+ version: result.planVersion.version,
1152
+ }),
1126
1153
  );
1127
1154
  }
1128
1155
  } catch (err: unknown) {
@@ -1140,19 +1167,21 @@ async function executePublish(): Promise<void> {
1140
1167
  }
1141
1168
  }
1142
1169
 
1143
- const REGRESSION_FIELD_LABELS: Record<string, string> = {
1144
- monthlyNet: 'Preis (monatlich, netto)',
1145
- yearlyNet: 'Preis (jährlich, netto)',
1146
- 'features.removed': 'Entfernte Features',
1147
- 'features.added': 'Hinzugefügte Features',
1148
- 'quotas.lowered': 'Gesenkte Quotas',
1149
- 'quotas.raised': 'Erhöhte Quotas',
1150
- 'bundles.removed': 'Entfernte Bundles',
1151
- 'bundles.added': 'Hinzugefügte Bundles',
1170
+ // API field path of a regression change → catalog key of its label.
1171
+ const REGRESSION_FIELD_KEYS: Record<string, keyof SaMessages['plans']['regressionFields']> = {
1172
+ monthlyNet: 'monthlyNet',
1173
+ yearlyNet: 'yearlyNet',
1174
+ 'features.removed': 'featuresRemoved',
1175
+ 'features.added': 'featuresAdded',
1176
+ 'quotas.lowered': 'quotasLowered',
1177
+ 'quotas.raised': 'quotasRaised',
1178
+ 'bundles.removed': 'bundlesRemoved',
1179
+ 'bundles.added': 'bundlesAdded',
1152
1180
  };
1153
1181
 
1154
1182
  function fieldLabel(field: string): string {
1155
- return REGRESSION_FIELD_LABELS[field] ?? field;
1183
+ const key = REGRESSION_FIELD_KEYS[field];
1184
+ return key ? msg.value.regressionFields[key] : field;
1156
1185
  }
1157
1186
 
1158
1187
  function formatChangeValue(value: unknown): string {
@@ -1175,21 +1204,24 @@ function describePublishError(err: unknown): string {
1175
1204
  const status = (err as { status?: number })?.status;
1176
1205
  const body = (err as { body?: { code?: string; message?: string } })?.body;
1177
1206
  if (status === 401 || status === 403) {
1178
- return 'Sitzung abgelaufen — bitte neu anmelden und erneut versuchen.';
1207
+ return msg.value.page.errorSessionExpiredPublish;
1179
1208
  }
1180
1209
  if (status === 422 && body?.code === 'PLAN_VERSION_REGRESSION') {
1181
- return 'Diese Version ist regressiv. Aktiviere "Force-Publish", wenn der Schritt absichtlich ist.';
1210
+ return msg.value.page.errorPublishRegression;
1182
1211
  }
1183
1212
  if (status === 422 && body?.code === 'PLAN_VERSION_VALID_FROM_REQUIRED') {
1184
- return 'Beim Publish muss "Gültig ab" gesetzt sein. Bitte den Draft öffnen und das Datum eintragen.';
1213
+ return msg.value.page.errorValidFromRequired;
1185
1214
  }
1186
1215
  if (status === 422 && body?.code === 'PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS') {
1187
- return '"Gültig ab" muss strikt nach dem "Gültig ab" der Vorgänger-Version liegen. Bitte ein späteres Datum wählen.';
1216
+ return msg.value.page.errorValidFromNotAfterPrevious;
1188
1217
  }
1189
1218
  if (status === 422 && body?.code === 'PLAN_VERSION_ZERO_PRICE') {
1190
- return 'Diese Version hat Preis 0,00 (Schutz gegen Seed-Platzhalter). Aktiviere "Preis 0,00 bewusst zulassen", wenn das ein gewollter kostenloser Sondervertrag ist.';
1219
+ return msg.value.page.errorZeroPrice;
1191
1220
  }
1192
- return body?.message ?? `Publish fehlgeschlagen (HTTP ${status ?? '?'})`;
1221
+ return (
1222
+ body?.message ??
1223
+ formatMessage(msg.value.page.errorPublishFailedHttp, { status: status ?? '?' })
1224
+ );
1193
1225
  }
1194
1226
 
1195
1227
  watch(plans, async () => {