@saasicat/ui-vue 0.15.0 → 0.16.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.
@@ -8,16 +8,28 @@
8
8
  // Without a provider the composables fall back to a shared German instance, so
9
9
  // isolated mounts and unit tests need no setup.
10
10
 
11
- import { computed, inject, isRef, ref, type ComputedRef, type InjectionKey, type Ref } from 'vue';
11
+ import {
12
+ computed,
13
+ inject,
14
+ isReadonly,
15
+ isRef,
16
+ ref,
17
+ watch,
18
+ type ComputedRef,
19
+ type InjectionKey,
20
+ type Ref,
21
+ } from 'vue';
12
22
 
13
23
  import {
14
24
  DEFAULT_SA_LOCALE,
15
25
  SA_INTL_LOCALES,
26
+ isSaLocale,
16
27
  resolveMessages,
17
28
  type SaLocale,
18
29
  type SaMessages,
19
30
  type SaMessagesOverrides,
20
31
  } from '../client/i18n/index.js';
32
+ import { defaultKvStore, type KvStore } from '../client/types.js';
21
33
 
22
34
  export interface SuperAdminI18n {
23
35
  /** Active UI locale — mutable; switching re-renders all catalog texts. */
@@ -26,18 +38,45 @@ export interface SuperAdminI18n {
26
38
  messages: ComputedRef<SaMessages>;
27
39
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
28
40
  intlLocale: ComputedRef<string>;
41
+ /**
42
+ * Whether the shell renders its language switcher. `false` when the app
43
+ * opted out or when `locale` cannot be written to — a switcher that cannot
44
+ * change anything is worse than none.
45
+ */
46
+ switcherEnabled: boolean;
29
47
  }
30
48
 
31
49
  export interface SuperAdminI18nOptions {
32
50
  /**
33
- * Initial UI locale, default `'de'`. Pass a `Ref` when the app wants to
34
- * keep control (e.g. a user-profile setting or a header switcher).
51
+ * Starting UI locale, default `'de'` — the shell's switcher lets the user
52
+ * pick another one, and a stored pick outranks this. Pass a `Ref` when the
53
+ * app wants to keep control (e.g. a user-profile setting).
35
54
  */
36
55
  locale?: SaLocale | Ref<SaLocale>;
37
56
  /** Per-locale string overrides layered over the platform catalog. */
38
57
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
58
+ /**
59
+ * Remembers the locale picked in the header switcher across reloads,
60
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
61
+ * and persists it wherever it belongs (user profile, its own store).
62
+ */
63
+ persist?: boolean;
64
+ /**
65
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`. Pass a
66
+ * key-prefixing wrapper when several apps share one origin.
67
+ */
68
+ storage?: KvStore;
69
+ /**
70
+ * Renders the language switcher in the shell chrome, default `true`. Set
71
+ * `false` for a deployment that ships one language. A readonly `locale`
72
+ * ref disables it on its own — no need to set this as well.
73
+ */
74
+ switcher?: boolean;
39
75
  }
40
76
 
77
+ /** `KvStore` key holding the locale the user picked in the switcher. */
78
+ export const SA_LOCALE_STORAGE_KEY = 'sa:locale';
79
+
41
80
  // `Symbol.for` — see super-admin-context.ts: dist- and src-imported module
42
81
  // instances must resolve to the same injection key.
43
82
  export const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n> = Symbol.for(
@@ -45,14 +84,38 @@ export const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n> = Symbol.for(
45
84
  );
46
85
 
47
86
  export function createSuperAdminI18n(options: SuperAdminI18nOptions = {}): SuperAdminI18n {
48
- const locale = isRef(options.locale)
49
- ? options.locale
50
- : ref(options.locale ?? DEFAULT_SA_LOCALE);
87
+ const locale = isRef(options.locale) ? options.locale : createOwnedLocale(options);
51
88
  const messages = computed(() =>
52
89
  resolveMessages(locale.value, options.overrides?.[locale.value]),
53
90
  );
54
91
  const intlLocale = computed(() => SA_INTL_LOCALES[locale.value]);
55
- return { locale, messages, intlLocale };
92
+ // A readonly ref (typically a `computed` derived from a profile setting)
93
+ // silently swallows writes, so the switcher would be a dead control. The
94
+ // type system permits it — TypeScript ignores `readonly` when checking
95
+ // assignability — so the guard has to be a runtime one.
96
+ const writable = !isReadonly(locale);
97
+ return {
98
+ locale,
99
+ messages,
100
+ intlLocale,
101
+ switcherEnabled: (options.switcher ?? true) && writable,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Builds the locale ref the platform owns. A stored choice outranks the
107
+ * `locale` option, because that option is the app's default while the stored
108
+ * value is the user's explicit pick in the switcher.
109
+ */
110
+ function createOwnedLocale(options: SuperAdminI18nOptions): Ref<SaLocale> {
111
+ if (options.persist === false) {
112
+ return ref(options.locale ?? DEFAULT_SA_LOCALE);
113
+ }
114
+ const storage = options.storage ?? defaultKvStore();
115
+ const stored = storage.get(SA_LOCALE_STORAGE_KEY);
116
+ const locale = ref(isSaLocale(stored) ? stored : (options.locale ?? DEFAULT_SA_LOCALE));
117
+ watch(locale, (next) => storage.set(SA_LOCALE_STORAGE_KEY, next));
118
+ return locale;
56
119
  }
57
120
 
58
121
  let fallbackI18n: SuperAdminI18n | null = null;
@@ -65,7 +128,10 @@ let fallbackI18n: SuperAdminI18n | null = null;
65
128
  export function useSuperAdminI18n(): SuperAdminI18n {
66
129
  const injected = inject(SUPER_ADMIN_I18N_KEY, null);
67
130
  if (injected) return injected;
68
- fallbackI18n ??= createSuperAdminI18n();
131
+ // Deliberately not persisted: this instance exists for mounts outside a
132
+ // shell, and a shared singleton that reads and writes real storage would
133
+ // make test outcomes depend on their order.
134
+ fallbackI18n ??= createSuperAdminI18n({ persist: false });
69
135
  return fallbackI18n;
70
136
  }
71
137