[data-reveal]{opacity:1!important;transform:none!important}
Frontend Composables

Utilities

i18n key resolution, unsaved-changes leave guarding, and onboarding state for layer authors.

The remaining composables round out a frontend layer's toolkit; each section leads with the one to reach for.

i18n

useTranslation — the $-prefix convention

Backend-driven screens pass strings that may be either a literal ("Save") or a translation key. The DMS disambiguates with a leading $: a value beginning with $ is an i18n key, anything else is shown verbatim. processI18n applies that rule:

resolving a label
const { processI18n } = useTranslation();

processI18n("Save"); // → "Save" (literal, returned as-is)
processI18n("$actions.save"); // → t("actions.save") — the leading $ is stripped
processI18n("$items.count", { n: 3 }); // params forwarded to t()

A custom builder can therefore accept user-authored labels and translation keys through the same prop — only the $-prefixed ones get looked up. Define the matching keys in your layer's locale files (Localization).

Outside component context — a Nuxt plugin, where useI18n() is unavailable — use the plugin-safe pair instead: getPluginI18n() returns the global composer plus a bound translate, and resolveI18nKey(translate, key, params?) applies the same $-prefix rule. This is how command-palette sources translate backend display names:

app/plugins/my-source.client.ts
const { i18n, translate } = getPluginI18n();
resolveI18nKey(translate, "$actions.save"); // → t("actions.save")
resolveI18nKey(translate, "Save"); // → "Save"

useUniqueLocales — for language switchers

useI18n().locales can list the same language code more than once. useUniqueLocales returns a computed deduplicated by code (first occurrence wins) — use it to populate a locale picker.

locale picker
const { uniqueLocales } = useUniqueLocales();
// uniqueLocales.value → one entry per code

Leave guard

usePageLeaveGuard — block navigation from a dirty form

The ergonomic entry point. It is page-scoped: guards are keyed by the current route and cleared automatically on unmount, so you register and forget.

registerGuard(callback) returns an unregister function. A callback that returns false (or throws) blocks navigation; returning anything else allows it. Callbacks may be async — guards are awaited.

unsaved form
const { registerGuard } = usePageLeaveGuard();
const { confirm } = useConfirm();
const isDirty = ref(false);

onMounted(() => {
  registerGuard(async () => {
    if (!isDirty.value) return true; // clean → allow
    return confirm({
      title: "Unsaved changes",
      description: "Discard them and leave?",
    }); // false → stay
  });
});

The dialog comes from useConfirm (UI toolkit), so the prompt matches the rest of the dashboard.

Guards only run client-side; during SSR registration is a no-op.

useLeaveGuard — the multi-guard primitive (advanced)

usePageLeaveGuard is a thin wrapper over useLeaveGuard, the lower-level registry. Reach for it only when guards must be scoped to something other than the current page — a multi-step container that outlives the route, for example. It exposes:

  • addGuard(containerId, callback) → returns an unregister function.
  • removeGuard(containerId, guardId) — remove a single guard.
  • executeGuards(containerId)Promise<boolean> — runs the container's guards in registration order; false if any returns false or throws.
  • clearGuards(containerId) — drop all guards for a container.

You own the containerId namespace and the cleanup.

Onboarding

useOnboarding — read onboarding state

useOnboarding() is async and resolves to a hasOnboarded flag. The first call fetches the state from the backend and caches it in shared state; subsequent calls return immediately.

read state
const { hasOnboarded } = await useOnboarding();

setOnboardingComplete — release the guard without a reload

The backend flips the onboarding flag when the first admin registers, but the cached client value stays stale until a full reload. Call setOnboardingComplete() to set the cached flag to true, so the global onboarding guard lets the user leave the wizard immediately — wire it to a wizard "finish" button.

wizard finish
async function finish() {
  // ...persist the wizard's result via your own backend route ($authFetch)...
  setOnboardingComplete(); // cached flag → true; no reload needed
  await navigateTo("/");
}
The funneling itself — non-onboarded users are redirected to /onboarding, and the wizard keeps already-onboarded users out — is built-in route middleware; you configure nothing for it.