@mapomodule/form 2.0.0-beta.5 → 2.0.0-beta.8

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 (54) hide show
  1. package/dist/module.d.ts.map +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +4 -1
  4. package/dist/runtime/components/MapoFocusPortal.vue +3 -1
  5. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -1
  6. package/dist/runtime/components/MapoFormTabs.vue +4 -2
  7. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -1
  8. package/dist/runtime/components/fields/MapoDateField.vue +3 -1
  9. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -1
  10. package/dist/runtime/components/fields/MapoDateTimeField.vue +5 -3
  11. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -1
  12. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +7 -5
  13. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -1
  14. package/dist/runtime/components/fields/MapoFileField.vue +3 -1
  15. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -1
  16. package/dist/runtime/components/fields/MapoFksField.vue +6 -2
  17. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -1
  18. package/dist/runtime/components/fields/MapoMapField.vue +4 -2
  19. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -1
  20. package/dist/runtime/components/fields/MapoMediaField.vue +3 -1
  21. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -1
  22. package/dist/runtime/components/fields/MapoMediaM2mField.vue +5 -1
  23. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -1
  24. package/dist/runtime/components/fields/MapoRepeater.vue +32 -22
  25. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -1
  26. package/dist/runtime/components/fields/MapoRepeaterItem.vue +13 -8
  27. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -1
  28. package/dist/runtime/components/fields/MapoSeoPreview.vue +16 -12
  29. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -1
  30. package/dist/runtime/components/fields/MapoWygEditor.vue +5 -1
  31. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -1
  32. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -1
  33. package/dist/runtime/composables/useMapoForm.js +4 -2
  34. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -1
  35. package/dist/runtime/composables/useUnsavedChangesGuard.js +3 -2
  36. package/package.json +9 -8
  37. package/src/__tests__/useMapoForm.test.ts +5 -0
  38. package/src/module.ts +9 -0
  39. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  40. package/src/runtime/components/MapoFormTabs.vue +4 -1
  41. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  42. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  43. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +8 -5
  44. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  45. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  46. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  47. package/src/runtime/components/fields/MapoMediaField.vue +4 -1
  48. package/src/runtime/components/fields/MapoMediaM2mField.vue +8 -1
  49. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  50. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  51. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  52. package/src/runtime/components/fields/MapoWygEditor.vue +8 -1
  53. package/src/runtime/composables/useMapoForm.ts +4 -2
  54. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import type { MediaM2mDescriptor, MediaItem } from "../../types/index.js";
4
5
  import { useMediaManager } from "../../composables/useMediaManager.js";
5
6
 
@@ -15,6 +16,8 @@ const emit = defineEmits<{
15
16
  "update:modelValue": [value: MediaItem[]];
16
17
  }>();
17
18
 
19
+ const { t } = useI18n();
20
+
18
21
  const dialogOpen = ref(false);
19
22
 
20
23
  // Picker dialog comes from @mapomodule/uikit, resolved at runtime.
@@ -101,7 +104,11 @@ function onDragEnd() {
101
104
  color="neutral"
102
105
  @click="dialogOpen = true"
103
106
  >
104
- {{ current.length > 0 ? "Add media" : "Select media" }}
107
+ {{
108
+ current.length > 0
109
+ ? t("mapo.mediaM2MField.addMedia")
110
+ : t("mapo.mediaField.selectMedia")
111
+ }}
105
112
  </UButton>
106
113
 
107
114
  <!-- Dialog -->
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watchEffect } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { VueDraggable } from "vue-draggable-plus";
4
5
  import { useConfirmStore } from "@mapomodule/store/runtime/stores/confirm";
5
6
  import type { RepeaterDescriptor } from "../../types/index.js";
@@ -17,6 +18,7 @@ const props = defineProps<{
17
18
  const emit = defineEmits<{ "update:modelValue": [value: unknown[]] }>();
18
19
 
19
20
  const form = injectMapoForm()!;
21
+ const { t } = useI18n();
20
22
 
21
23
  const attrs = computed(() => props.descriptor.attrs ?? {});
22
24
 
@@ -108,10 +110,8 @@ function minBound(): number {
108
110
  return attrs.value.minItems ?? 0;
109
111
  }
110
112
 
111
- async function askConfirm(
112
- message: string,
113
- title = "Confirm",
114
- ): Promise<boolean> {
113
+ async function askConfirm(message: string, title?: string): Promise<boolean> {
114
+ title ??= t("mapo.confirm");
115
115
  try {
116
116
  // Falls back to native confirm when the Pinia store is not registered.
117
117
  return await useConfirmStore().ask({ title, message });
@@ -135,8 +135,8 @@ async function deleteItem(index: number) {
135
135
  if (!canDelete.value) return;
136
136
  if (attrs.value.confirmDelete !== false) {
137
137
  const confirmed = await askConfirm(
138
- "Are you sure you want to delete this item?",
139
- "Delete item",
138
+ t("mapo.confirmDelete"),
139
+ t("mapo.repeater.deleteItem"),
140
140
  );
141
141
  if (!confirmed) return;
142
142
  }
@@ -275,14 +275,14 @@ async function bulkDelete() {
275
275
  // Enforce minItems: would deletion drop below the lower bound?
276
276
  if (items.value.length - count < minBound()) {
277
277
  await askConfirm(
278
- `Cannot delete: at least ${minBound()} items must remain.`,
279
- "Action blocked",
278
+ t("mapo.repeater.minItemsBlock", { min: minBound() }),
279
+ t("mapo.repeater.actionBlocked"),
280
280
  );
281
281
  return;
282
282
  }
283
283
  const confirmed = await askConfirm(
284
- `Delete ${count} selected items?`,
285
- "Delete items",
284
+ t("mapo.repeater.confirmBulkDelete", { count }),
285
+ t("mapo.repeater.deleteItems"),
286
286
  );
287
287
  if (!confirmed) return;
288
288
  pushUndo();
@@ -336,7 +336,7 @@ const errorPrefix = (index: number) =>
336
336
  class="flex flex-wrap items-center gap-2 rounded border border-gray-200 bg-gray-50 px-3 py-2"
337
337
  >
338
338
  <span class="text-sm font-medium text-gray-700">
339
- {{ items.length }} {{ items.length === 1 ? "item" : "items" }}
339
+ {{ t("mapo.repeater.nItems", { n: items.length }, items.length) }}
340
340
  <span v-if="attrs.maxItems" class="text-gray-400"
341
341
  >/ {{ attrs.maxItems }}</span
342
342
  >
@@ -344,10 +344,10 @@ const errorPrefix = (index: number) =>
344
344
 
345
345
  <div class="flex flex-1 flex-wrap items-center justify-end gap-2">
346
346
  <UButton size="xs" variant="ghost" color="neutral" @click="collapseAll">
347
- Collapse all
347
+ {{ t("mapo.repeater.collapseAll") }}
348
348
  </UButton>
349
349
  <UButton size="xs" variant="ghost" color="neutral" @click="expandAll">
350
- Expand all
350
+ {{ t("mapo.repeater.expandAll") }}
351
351
  </UButton>
352
352
  <UButton
353
353
  v-if="undoStack.length"
@@ -355,7 +355,7 @@ const errorPrefix = (index: number) =>
355
355
  variant="ghost"
356
356
  color="neutral"
357
357
  icon="i-lucide-undo-2"
358
- title="Undo last action"
358
+ :title="t('mapo.repeater.undoLastAction')"
359
359
  @click="undo"
360
360
  />
361
361
 
@@ -365,8 +365,10 @@ const errorPrefix = (index: number) =>
365
365
  :variant="selectionMode ? 'solid' : 'ghost'"
366
366
  color="neutral"
367
367
  icon="i-lucide-check-square"
368
- title="Bulk selection"
369
- :label="selectionMode ? 'Exit selection' : 'Select'"
368
+ :title="t('mapo.repeater.bulkSelection')"
369
+ :label="
370
+ selectionMode ? t('mapo.repeater.exitSelection') : t('mapo.select')
371
+ "
370
372
  @click="toggleSelectionMode"
371
373
  />
372
374
 
@@ -386,7 +388,7 @@ const errorPrefix = (index: number) =>
386
388
  :disabled="!canAdd"
387
389
  @click="addItem"
388
390
  >
389
- Add
391
+ {{ t("mapo.add") }}
390
392
  </UButton>
391
393
  </div>
392
394
  </div>
@@ -398,11 +400,11 @@ const errorPrefix = (index: number) =>
398
400
  class="flex items-center gap-2 rounded border border-primary-200 bg-primary-50 px-3 py-2"
399
401
  >
400
402
  <span class="text-sm font-medium text-primary-700">
401
- {{ selectionCount }} selected
403
+ {{ t("mapo.repeater.nSelected", { n: selectionCount }) }}
402
404
  </span>
403
405
  <div class="flex flex-1 flex-wrap items-center gap-2">
404
406
  <UButton size="xs" variant="ghost" color="neutral" @click="selectAll">
405
- All
407
+ {{ t("mapo.repeater.all") }}
406
408
  </UButton>
407
409
  <UButton
408
410
  size="xs"
@@ -410,7 +412,7 @@ const errorPrefix = (index: number) =>
410
412
  color="neutral"
411
413
  @click="clearSelection"
412
414
  >
413
- Clear
415
+ {{ t("mapo.clear") }}
414
416
  </UButton>
415
417
  <UButton
416
418
  size="xs"
@@ -420,12 +422,17 @@ const errorPrefix = (index: number) =>
420
422
  :disabled="remainingSlots() <= 0"
421
423
  :title="
422
424
  remainingSlots() < selectionCount
423
- ? `Only ${remainingSlots()} of ${selectionCount} can be duplicated (maxItems reached)`
424
- : 'Duplicate selected'
425
+ ? t('mapo.repeater.duplicateLimited', {
426
+ slots: remainingSlots(),
427
+ count: selectionCount,
428
+ })
429
+ : t('mapo.repeater.duplicateSelected')
425
430
  "
426
431
  @click="bulkDuplicate"
427
432
  >
428
- Duplicate ({{ Math.min(selectionCount, remainingSlots()) }})
433
+ {{ t("mapo.duplicate") }} ({{
434
+ Math.min(selectionCount, remainingSlots())
435
+ }})
429
436
  </UButton>
430
437
  <UButton
431
438
  size="xs"
@@ -434,7 +441,7 @@ const errorPrefix = (index: number) =>
434
441
  icon="i-lucide-arrow-up"
435
442
  @click="bulkMoveToTop"
436
443
  >
437
- Move to top
444
+ {{ t("mapo.repeater.moveToTop") }}
438
445
  </UButton>
439
446
  <UButton
440
447
  size="xs"
@@ -444,7 +451,7 @@ const errorPrefix = (index: number) =>
444
451
  :disabled="!canDelete"
445
452
  @click="bulkDelete"
446
453
  >
447
- Delete ({{ selectionCount }})
454
+ {{ t("mapo.delete") }} ({{ selectionCount }})
448
455
  </UButton>
449
456
  </div>
450
457
  </div>
@@ -527,7 +534,7 @@ const errorPrefix = (index: number) =>
527
534
  v-if="items.length === 0"
528
535
  class="rounded border border-dashed border-gray-300 py-8 text-center text-sm text-gray-400"
529
536
  >
530
- No items. Click "Add" to start.
537
+ {{ t("mapo.repeater.empty") }}
531
538
  </div>
532
539
  </div>
533
540
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch, watchEffect } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import type {
4
5
  AnyFieldDescriptor,
5
6
  FieldRegistry,
@@ -9,6 +10,8 @@ import { useMapoForm, injectMapoForm } from "../../composables/useMapoForm.js";
9
10
  import { useFocusMode } from "../../composables/useFocusMode.js";
10
11
  import MapoFormField from "../MapoFormField.vue";
11
12
 
13
+ const { t } = useI18n();
14
+
12
15
  const props = defineProps<{
13
16
  /** Stable identity-based UID assigned by the parent repeater. */
14
17
  uid?: string;
@@ -173,7 +176,10 @@ function enterFocusMode() {
173
176
  registry: props.registry,
174
177
  languages: props.languages,
175
178
  currentLang: props.currentLang,
176
- breadcrumb: [props.repeaterDescriptor?.label ?? "Repeater", label.value],
179
+ breadcrumb: [
180
+ props.repeaterDescriptor?.label ?? t("mapo.repeater.repeater"),
181
+ label.value,
182
+ ],
177
183
  });
178
184
  }
179
185
  </script>
@@ -246,7 +252,7 @@ function enterFocusMode() {
246
252
  color="error"
247
253
  icon="i-lucide-trash-2"
248
254
  :disabled="readonly"
249
- title="Delete"
255
+ :title="t('mapo.delete')"
250
256
  @click.stop="$emit('delete')"
251
257
  />
252
258
  <UIcon name="i-lucide-chevron-down" class="h-4 w-4 text-gray-400" />
@@ -307,7 +313,7 @@ function enterFocusMode() {
307
313
  :max="totalItems"
308
314
  size="xs"
309
315
  class="w-14 text-center"
310
- title="Position"
316
+ :title="t('mapo.repeater.position')"
311
317
  @change="
312
318
  $emit(
313
319
  'move-to',
@@ -329,7 +335,7 @@ function enterFocusMode() {
329
335
  variant="ghost"
330
336
  color="neutral"
331
337
  icon="i-lucide-maximize-2"
332
- title="Edit full screen"
338
+ :title="t('mapo.repeater.editFullScreen')"
333
339
  @click="enterFocusMode"
334
340
  />
335
341
 
@@ -339,7 +345,7 @@ function enterFocusMode() {
339
345
  color="neutral"
340
346
  icon="i-lucide-chevron-up"
341
347
  :disabled="readonly"
342
- title="Move up"
348
+ :title="t('mapo.repeater.moveUp')"
343
349
  @click="$emit('move-up')"
344
350
  />
345
351
  <UButton
@@ -348,7 +354,7 @@ function enterFocusMode() {
348
354
  color="neutral"
349
355
  icon="i-lucide-chevron-down"
350
356
  :disabled="readonly"
351
- title="Move down"
357
+ :title="t('mapo.repeater.moveDown')"
352
358
  @click="$emit('move-down')"
353
359
  />
354
360
 
@@ -359,7 +365,7 @@ function enterFocusMode() {
359
365
  color="neutral"
360
366
  icon="i-lucide-copy"
361
367
  :disabled="readonly"
362
- title="Duplicate"
368
+ :title="t('mapo.duplicate')"
363
369
  @click="$emit('duplicate')"
364
370
  />
365
371
 
@@ -369,7 +375,7 @@ function enterFocusMode() {
369
375
  color="error"
370
376
  icon="i-lucide-trash-2"
371
377
  :disabled="readonly"
372
- title="Delete"
378
+ :title="t('mapo.delete')"
373
379
  @click="$emit('delete')"
374
380
  />
375
381
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { useRequestURL } from "#app";
4
5
  import type { SeoDescriptor } from "../../types/index.js";
5
6
 
@@ -10,6 +11,8 @@ interface SeoValue {
10
11
  permalink?: string;
11
12
  }
12
13
 
14
+ const { t } = useI18n();
15
+
13
16
  const props = defineProps<{
14
17
  modelValue: unknown;
15
18
  descriptor: SeoDescriptor;
@@ -58,7 +61,9 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
58
61
  <div class="space-y-3">
59
62
  <div>
60
63
  <div class="mb-1 flex items-center justify-between">
61
- <label class="text-sm font-medium text-gray-700">URL</label>
64
+ <label class="text-sm font-medium text-gray-700">{{
65
+ t("mapo.url")
66
+ }}</label>
62
67
  </div>
63
68
  <UInput
64
69
  :model-value="value.permalink"
@@ -72,14 +77,16 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
72
77
 
73
78
  <div>
74
79
  <div class="mb-1 flex items-center justify-between">
75
- <label class="text-sm font-medium text-gray-700">SEO title</label>
80
+ <label class="text-sm font-medium text-gray-700">{{
81
+ t("mapo.seoPreview.seoTitle")
82
+ }}</label>
76
83
  <span class="text-xs" :class="titleColor">{{ titleLen }} / 60</span>
77
84
  </div>
78
85
  <UInput
79
86
  :model-value="value.title"
80
87
  :readonly="readonly"
81
88
  :disabled="disabled"
82
- placeholder="Page title..."
89
+ :placeholder="t('mapo.seoPreview.titlePlaceholder')"
83
90
  class="w-full"
84
91
  @update:model-value="update('title', $event as string)"
85
92
  />
@@ -87,9 +94,9 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
87
94
 
88
95
  <div>
89
96
  <div class="mb-1 flex items-center justify-between">
90
- <label class="text-sm font-medium text-gray-700"
91
- >Meta description</label
92
- >
97
+ <label class="text-sm font-medium text-gray-700">{{
98
+ t("mapo.seoPreview.metaDescription")
99
+ }}</label>
93
100
  <span class="text-xs" :class="descColor">{{ descLen }} / 155</span>
94
101
  </div>
95
102
  <UTextarea
@@ -97,7 +104,7 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
97
104
  :rows="3"
98
105
  :readonly="readonly"
99
106
  :disabled="disabled"
100
- placeholder="Page description..."
107
+ :placeholder="t('mapo.seoPreview.descriptionPlaceholder')"
101
108
  class="w-full"
102
109
  @update:model-value="update('description', $event as string)"
103
110
  />
@@ -107,20 +114,17 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
107
114
  <!-- SERP preview -->
108
115
  <div class="rounded border border-gray-200 bg-gray-50 p-4">
109
116
  <p class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-400">
110
- Google preview
117
+ {{ t("mapo.seoPreview.googlePreview") }}
111
118
  </p>
112
119
  <div class="max-w-lg">
113
120
  <p class="truncate text-sm text-green-700">
114
121
  {{ displayUrl }}
115
122
  </p>
116
123
  <p class="truncate text-lg text-blue-700 underline">
117
- {{ value.title || "Page title" }}
124
+ {{ value.title || t("mapo.seoPreview.defaultTitle") }}
118
125
  </p>
119
126
  <p class="mt-1 line-clamp-2 text-sm text-gray-600">
120
- {{
121
- value.description ||
122
- "Page description that will appear in Google search results."
123
- }}
127
+ {{ value.description || t("mapo.seoPreview.defaultDescription") }}
124
128
  </p>
125
129
  </div>
126
130
  </div>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, onBeforeUnmount, resolveComponent } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { useEditor, EditorContent } from "@tiptap/vue-3";
4
5
  import type { AnyExtension } from "@tiptap/vue-3";
5
6
  import StarterKit from "@tiptap/starter-kit";
@@ -18,6 +19,8 @@ const props = defineProps<{
18
19
 
19
20
  const emit = defineEmits<{ "update:modelValue": [value: string] }>();
20
21
 
22
+ const { t } = useI18n();
23
+
21
24
  const extraExtensions =
22
25
  (props.descriptor.attrs?.extensions as AnyExtension[] | undefined) ?? [];
23
26
 
@@ -249,7 +252,11 @@ function onImageConfirm(
249
252
 
250
253
  <!-- Insert Image via Media Manager (available when @mapomodule/uikit is installed) -->
251
254
  <UTooltip
252
- :text="hasMediaManager ? 'Insert image' : 'Requires @mapomodule/uikit'"
255
+ :text="
256
+ hasMediaManager
257
+ ? t('mapo.wygEditor.insertImage')
258
+ : t('mapo.wygEditor.requiresUikit')
259
+ "
253
260
  :delay-open="300"
254
261
  >
255
262
  <UButton
@@ -19,6 +19,7 @@ import type { AnyFieldDescriptor, FieldRegistry } from "../types/index.js";
19
19
  import { resolveFieldAccessor } from "../types/index.js";
20
20
  import { useCurrentLang } from "./useCurrentLang.js";
21
21
  import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
22
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
22
23
  import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
23
24
 
24
25
  /** Options accepted by `useMapoForm()`. */
@@ -121,6 +122,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
121
122
 
122
123
  const currentLang = useCurrentLang(currentLangProp);
123
124
  const snack = useSnackStore();
125
+ const t = useMapoT();
124
126
  const authStore = useAuthStore();
125
127
  const backup = ref(safeClone(model.value)) as Ref<T>;
126
128
  const clientErrors = ref<Record<string, string>>({});
@@ -318,7 +320,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
318
320
  ): string | null {
319
321
  if (!descriptor.required) return null;
320
322
  if (val === null || val === undefined || val === "")
321
- return "This field is required.";
323
+ return t("mapo.form.required");
322
324
  return null;
323
325
  }
324
326
 
@@ -379,7 +381,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
379
381
  submitted.value = true;
380
382
  const { valid } = validateClient();
381
383
  if (!valid) {
382
- snack.show("Please fix the errors before saving.", "error");
384
+ snack.show(t("mapo.fixErrors"), "error");
383
385
  return;
384
386
  }
385
387
  isLoading.value = true;
@@ -1,5 +1,6 @@
1
1
  import { onBeforeUnmount, type Ref } from "vue";
2
- import { onBeforeRouteLeave } from "vue-router";
2
+ import { onBeforeRouteLeave } from "#app";
3
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
3
4
 
4
5
  /** Optional configuration for the unsaved-changes navigation guard. */
5
6
  export interface UnsavedChangesGuardOptions {
@@ -27,8 +28,7 @@ export function useUnsavedChangesGuard(
27
28
  isDirty: Ref<boolean>,
28
29
  options: UnsavedChangesGuardOptions = {},
29
30
  ) {
30
- const message =
31
- options.message ?? "You have unsaved changes. Do you really want to leave?";
31
+ const message = options.message ?? useMapoT()("mapo.confirmLeaveChanges");
32
32
  const canUseWindow = typeof window !== "undefined";
33
33
 
34
34
  const isEnabled = (): boolean => {