@saasicat/ui-vue 0.15.1 → 0.17.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 (43) hide show
  1. package/README.md +13 -10
  2. package/dist/{messages-BIYw32u1.d.ts → catalog-C4Gv2_KM.d.cts} +126 -39
  3. package/dist/{messages-BIYw32u1.d.cts → catalog-C4Gv2_KM.d.ts} +126 -39
  4. package/dist/{chunk-TY2WYKME.js → chunk-247RJPU4.js} +38 -11
  5. package/dist/{chunk-TBLXKSTR.js → chunk-IZGFS5RS.js} +90 -21
  6. package/dist/{chunk-66BUSVC5.js → chunk-XPSD5HKG.js} +30 -4
  7. package/dist/client/index.cjs +122 -22
  8. package/dist/client/index.d.cts +51 -5
  9. package/dist/client/index.d.ts +51 -5
  10. package/dist/client/index.js +14 -2
  11. package/dist/index.cjs +152 -30
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +17 -3
  15. package/dist/quasar/index.cjs +110 -14
  16. package/dist/quasar/index.d.cts +2 -2
  17. package/dist/quasar/index.d.ts +2 -2
  18. package/dist/quasar/index.js +2 -2
  19. package/dist/{use-super-admin-i18n-DL_qI1BY.d.cts → use-super-admin-i18n-BdPqeTHl.d.cts} +51 -5
  20. package/dist/{use-super-admin-i18n-CWNu_W3u.d.ts → use-super-admin-i18n-uteRCL9q.d.ts} +51 -5
  21. package/package.json +8 -3
  22. package/src/client/i18n/catalog.ts +120 -0
  23. package/src/client/i18n/currency.ts +7 -2
  24. package/src/client/i18n/define.ts +2 -2
  25. package/src/client/i18n/index.ts +1 -0
  26. package/src/client/i18n/locale.ts +52 -8
  27. package/src/client/i18n/messages/bundles.ts +2 -3
  28. package/src/client/i18n/messages/marketing.ts +2 -2
  29. package/src/client/i18n/messages/nav.ts +3 -3
  30. package/src/client/i18n/messages/shell.ts +2 -0
  31. package/src/client/i18n/messages.ts +7 -4
  32. package/src/client/index.ts +1 -0
  33. package/src/client/login-branding.ts +61 -0
  34. package/src/client/nav-builder.ts +33 -6
  35. package/src/client/types.ts +51 -14
  36. package/src/components/LocaleSwitcher.vue +77 -0
  37. package/src/components/bundle-editor/bundle-version-status.ts +16 -8
  38. package/src/pages-standard/AdminLayout.vue +9 -3
  39. package/src/pages-standard/SuperAdminLoginPage.vue +17 -9
  40. package/src/pages-standard/SuperAdminSetupWizard.vue +7 -0
  41. package/src/pages-standard/discovery-page/discovery-ui.ts +8 -8
  42. package/src/pages-standard/plan-versions/format.ts +2 -2
  43. package/src/vue/use-super-admin-i18n.ts +116 -19
package/dist/index.js CHANGED
@@ -16,11 +16,14 @@ import {
16
16
  createAdminResourceClient,
17
17
  defaultSectionOrder,
18
18
  getJson,
19
+ isProductionBoot,
19
20
  postJson,
20
21
  resolveExtension,
22
+ resolveLoginBranding,
21
23
  trimTrailingSlashes
22
- } from "./chunk-66BUSVC5.js";
24
+ } from "./chunk-XPSD5HKG.js";
23
25
  import {
26
+ SA_LOCALE_STORAGE_KEY,
24
27
  SUPER_ADMIN_ACTIONS_KEY,
25
28
  SUPER_ADMIN_BRAND_KEY,
26
29
  SUPER_ADMIN_ENDPOINTS_KEY,
@@ -34,20 +37,24 @@ import {
34
37
  createSuperAdminI18n,
35
38
  useSaMessages,
36
39
  useSuperAdminI18n
37
- } from "./chunk-TY2WYKME.js";
40
+ } from "./chunk-247RJPU4.js";
38
41
  import {
39
42
  DEFAULT_SA_LOCALE,
40
43
  SA_INTL_LOCALES,
41
44
  SA_LOCALES,
45
+ SA_LOCALE_LABELS,
42
46
  SA_MESSAGES,
47
+ builtinLocaleOf,
48
+ createSaCatalog,
43
49
  defaultHttpClient,
44
50
  defaultKvStore,
45
51
  defineMessages,
46
52
  formatCurrency,
47
53
  formatMessage,
54
+ isSaBuiltinLocale,
48
55
  mergeMessages,
49
56
  resolveMessages
50
- } from "./chunk-TBLXKSTR.js";
57
+ } from "./chunk-IZGFS5RS.js";
51
58
 
52
59
  // src/vue/use-super-admin-context.ts
53
60
  import { inject } from "vue";
@@ -2815,6 +2822,8 @@ export {
2815
2822
  PromotionsApiError,
2816
2823
  SA_INTL_LOCALES,
2817
2824
  SA_LOCALES,
2825
+ SA_LOCALE_LABELS,
2826
+ SA_LOCALE_STORAGE_KEY,
2818
2827
  SA_MESSAGES,
2819
2828
  SUPER_ADMIN_ACTIONS_KEY,
2820
2829
  SUPER_ADMIN_BRAND_KEY,
@@ -2832,11 +2841,13 @@ export {
2832
2841
  buildQuotaRegistry,
2833
2842
  buildRoutes,
2834
2843
  buildSidebar,
2844
+ builtinLocaleOf,
2835
2845
  createAdminResourceClient,
2836
2846
  createAdminRoutes,
2837
2847
  createManifestStore,
2838
2848
  createPlatformLoaders,
2839
2849
  createProjectPageHostRoute,
2850
+ createSaCatalog,
2840
2851
  createSuperAdminI18n,
2841
2852
  defaultHttpClient,
2842
2853
  defaultIconForActionKey,
@@ -2848,10 +2859,13 @@ export {
2848
2859
  formatCurrency,
2849
2860
  formatMessage,
2850
2861
  getJson,
2862
+ isProductionBoot,
2863
+ isSaBuiltinLocale,
2851
2864
  mergeMessages,
2852
2865
  postJson,
2853
2866
  provideEntitlement,
2854
2867
  resolveExtension,
2868
+ resolveLoginBranding,
2855
2869
  resolveMessages,
2856
2870
  trimTrailingSlashes,
2857
2871
  useActions,
@@ -33,6 +33,45 @@ var import_pinia = require("pinia");
33
33
  var import_vue_router = require("vue-router");
34
34
 
35
35
  // src/client/types.ts
36
+ var NOOP_KV_STORE = {
37
+ get: () => null,
38
+ set: () => {
39
+ },
40
+ remove: () => {
41
+ }
42
+ };
43
+ function defaultKvStore() {
44
+ const ls = resolveLocalStorage();
45
+ if (!ls) return NOOP_KV_STORE;
46
+ return {
47
+ get: (k) => {
48
+ try {
49
+ return ls.getItem(k);
50
+ } catch {
51
+ return null;
52
+ }
53
+ },
54
+ set: (k, v) => {
55
+ try {
56
+ ls.setItem(k, v);
57
+ } catch {
58
+ }
59
+ },
60
+ remove: (k) => {
61
+ try {
62
+ ls.removeItem(k);
63
+ } catch {
64
+ }
65
+ }
66
+ };
67
+ }
68
+ function resolveLocalStorage() {
69
+ try {
70
+ return globalThis.localStorage ?? null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
36
75
  function defaultHttpClient() {
37
76
  return (url, init) => fetch(url, init);
38
77
  }
@@ -93,11 +132,19 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
93
132
  var import_vue = require("vue");
94
133
 
95
134
  // src/client/i18n/locale.ts
135
+ var SA_LOCALES = ["de", "en"];
96
136
  var DEFAULT_SA_LOCALE = "de";
97
137
  var SA_INTL_LOCALES = {
98
138
  de: "de-DE",
99
139
  en: "en-US"
100
140
  };
141
+ var SA_LOCALE_LABELS = {
142
+ de: "Deutsch",
143
+ en: "English"
144
+ };
145
+ function isSaBuiltinLocale(value) {
146
+ return typeof value === "string" && SA_LOCALES.includes(value);
147
+ }
101
148
 
102
149
  // src/client/i18n/define.ts
103
150
  function defineMessages(de, en) {
@@ -270,7 +317,7 @@ var bundlesMessages = defineMessages(
270
317
  bundleKeyHint: "API-stabil \xB7 wird aus dem Label erzeugt",
271
318
  errorKeyFormat: "Nur A-Z, 0-9, Underscore; muss mit Buchstabe beginnen.",
272
319
  errorKeyExists: "Dieser Bundle-Key existiert bereits.",
273
- descriptionPlaceholder: "z. B. Kampagnen, WhatsApp und Korrespondenz f\xFCr aktive Vereine.",
320
+ descriptionPlaceholder: "z. B. Kampagnen, WhatsApp und Korrespondenz.",
274
321
  sectionPricing: "v1 \xB7 Pricing & G\xFCltigkeit",
275
322
  pricingHint: "Monats- & Jahrespreis sowie Datum, ab dem das Bundle verkaufbar wird.",
276
323
  validFromImmediate: "\u2713 Bundle ist nach Anlage sofort live und verkaufbar.",
@@ -482,7 +529,7 @@ var bundlesMessages = defineMessages(
482
529
  bundleKeyHint: "API-stable \xB7 derived from the label",
483
530
  errorKeyFormat: "Only A-Z, 0-9, underscore; must start with a letter.",
484
531
  errorKeyExists: "This bundle key already exists.",
485
- descriptionPlaceholder: "e.g. Campaigns, WhatsApp and correspondence for active clubs.",
532
+ descriptionPlaceholder: "e.g. Campaigns, WhatsApp and correspondence.",
486
533
  sectionPricing: "v1 \xB7 pricing & validity",
487
534
  pricingHint: "Monthly & yearly price plus the date from which the bundle becomes sellable.",
488
535
  validFromImmediate: "\u2713 The bundle is live and sellable right after creation.",
@@ -1156,7 +1203,7 @@ var marketingMessages = defineMessages(
1156
1203
  ctaOverrideLabel: "CTA-Text \xFCberschreiben (optional)",
1157
1204
  ctaOverrideHint: "leer lassen f\xFCr Auto-Text",
1158
1205
  topFeatureCount: "{count} Eintr\xE4ge",
1159
- featureLabelPlaceholder: "z. B. Beitragsverwaltung",
1206
+ featureLabelPlaceholder: "z. B. Erweiterte Berichte",
1160
1207
  featureStrongPlaceholder: "Highlight, z. B. bis 100",
1161
1208
  moveUp: "Nach oben",
1162
1209
  moveDown: "Nach unten",
@@ -1284,7 +1331,7 @@ var marketingMessages = defineMessages(
1284
1331
  ctaOverrideLabel: "Override CTA text (optional)",
1285
1332
  ctaOverrideHint: "leave empty for the auto text",
1286
1333
  topFeatureCount: "{count} entries",
1287
- featureLabelPlaceholder: "e.g. Membership management",
1334
+ featureLabelPlaceholder: "e.g. Advanced reporting",
1288
1335
  featureStrongPlaceholder: "Highlight, e.g. up to 100",
1289
1336
  moveUp: "Move up",
1290
1337
  moveDown: "Move down",
@@ -2777,7 +2824,8 @@ var shellMessages = defineMessages(
2777
2824
  productionWarning: "Aktionen wirken sich sofort auf alle Mandanten aus.",
2778
2825
  subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
2779
2826
  roleBadge: "SUPER ADMIN",
2780
- logout: "Abmelden"
2827
+ logout: "Abmelden",
2828
+ language: "Sprache"
2781
2829
  },
2782
2830
  drawer: {
2783
2831
  docs: "Doku \xF6ffnen"
@@ -2845,7 +2893,8 @@ var shellMessages = defineMessages(
2845
2893
  productionWarning: "Actions take effect immediately for all tenants.",
2846
2894
  subtitle: "SuperAdmin \xB7 Platform administration",
2847
2895
  roleBadge: "SUPER ADMIN",
2848
- logout: "Sign out"
2896
+ logout: "Sign out",
2897
+ language: "Language"
2849
2898
  },
2850
2899
  drawer: {
2851
2900
  docs: "Open documentation"
@@ -3113,22 +3162,69 @@ var SA_MESSAGES = {
3113
3162
  de: catalogFor("de"),
3114
3163
  en: catalogFor("en")
3115
3164
  };
3116
- function resolveMessages(locale, overrides) {
3117
- if (!overrides) return SA_MESSAGES[locale];
3118
- return mergeMessages(SA_MESSAGES[locale], overrides);
3165
+
3166
+ // src/client/i18n/catalog.ts
3167
+ function createSaCatalog(options = {}) {
3168
+ const additional = options.additionalLocales ?? {};
3169
+ const known = /* @__PURE__ */ new Set([...SA_LOCALES, ...Object.keys(additional)]);
3170
+ const requested = options.locales?.filter((code) => known.has(code));
3171
+ const offered = requested && requested.length > 0 ? requested : [...SA_LOCALES, ...Object.keys(additional)];
3172
+ const labelFor = (locale) => additional[locale]?.label ?? (isSaBuiltinLocale(locale) ? SA_LOCALE_LABELS[locale] : locale);
3173
+ const cache = /* @__PURE__ */ new Map();
3174
+ const messagesFor = (locale) => {
3175
+ const hit = cache.get(locale);
3176
+ if (hit) return hit;
3177
+ const definition = additional[locale];
3178
+ const base = SA_MESSAGES[definition?.basedOn ?? (isSaBuiltinLocale(locale) ? locale : "en")];
3179
+ const translated = definition ? mergeMessages(base, definition.messages) : base;
3180
+ const resolved = mergeMessages(translated, options.overrides?.[locale]);
3181
+ cache.set(locale, resolved);
3182
+ return resolved;
3183
+ };
3184
+ const offeredSet = new Set(offered);
3185
+ return {
3186
+ locales: offered.map((code) => ({ code, label: labelFor(code) })),
3187
+ has: (locale) => offeredSet.has(locale),
3188
+ // The app's first offered locale beats the platform default: an app
3189
+ // shipping English only must not start in German.
3190
+ defaultLocale: offered.includes(DEFAULT_SA_LOCALE) ? DEFAULT_SA_LOCALE : offered[0],
3191
+ messagesFor,
3192
+ intlLocaleFor: (locale) => additional[locale]?.intlLocale ?? (isSaBuiltinLocale(locale) ? SA_INTL_LOCALES[locale] : locale),
3193
+ labelFor
3194
+ };
3119
3195
  }
3120
3196
 
3121
3197
  // src/vue/use-super-admin-i18n.ts
3198
+ var SA_LOCALE_STORAGE_KEY = "sa:locale";
3122
3199
  var SUPER_ADMIN_I18N_KEY = /* @__PURE__ */ Symbol.for(
3123
3200
  "@saasicat/ui-vue/SUPER_ADMIN_I18N"
3124
3201
  );
3125
3202
  function createSuperAdminI18n(options = {}) {
3126
- const locale = (0, import_vue.isRef)(options.locale) ? options.locale : (0, import_vue.ref)(options.locale ?? DEFAULT_SA_LOCALE);
3127
- const messages = (0, import_vue.computed)(
3128
- () => resolveMessages(locale.value, options.overrides?.[locale.value])
3203
+ const catalog = createSaCatalog(options);
3204
+ const locale = (0, import_vue.isRef)(options.locale) ? options.locale : createOwnedLocale(options, catalog);
3205
+ const active = (0, import_vue.computed)(
3206
+ () => catalog.has(locale.value) ? locale.value : catalog.defaultLocale
3129
3207
  );
3130
- const intlLocale = (0, import_vue.computed)(() => SA_INTL_LOCALES[locale.value]);
3131
- return { locale, messages, intlLocale };
3208
+ const messages = (0, import_vue.computed)(() => catalog.messagesFor(active.value));
3209
+ const intlLocale = (0, import_vue.computed)(() => catalog.intlLocaleFor(active.value));
3210
+ const writable = !(0, import_vue.isReadonly)(locale);
3211
+ return {
3212
+ locale,
3213
+ messages,
3214
+ intlLocale,
3215
+ switcherEnabled: (options.switcher ?? true) && writable && catalog.locales.length > 1,
3216
+ availableLocales: catalog.locales
3217
+ };
3218
+ }
3219
+ function createOwnedLocale(options, catalog) {
3220
+ const initial = options.locale ?? catalog.defaultLocale;
3221
+ if (options.persist === false) return (0, import_vue.ref)(initial);
3222
+ const storage = options.storage ?? defaultKvStore();
3223
+ const key = `${options.storageKeyPrefix ?? ""}${SA_LOCALE_STORAGE_KEY}`;
3224
+ const stored = storage.get(key);
3225
+ const locale = (0, import_vue.ref)(stored && catalog.has(stored) ? stored : initial);
3226
+ (0, import_vue.watch)(locale, (next) => storage.set(key, next));
3227
+ return locale;
3132
3228
  }
3133
3229
 
3134
3230
  // src/quasar/notify.ts
@@ -2,8 +2,8 @@ import { Component, App } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
- import { H as HttpClient } from '../messages-BIYw32u1.cjs';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-DL_qI1BY.cjs';
5
+ import { H as HttpClient } from '../catalog-C4Gv2_KM.cjs';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-BdPqeTHl.cjs';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -2,8 +2,8 @@ import { Component, App } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
- import { H as HttpClient } from '../messages-BIYw32u1.js';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-CWNu_W3u.js';
5
+ import { H as HttpClient } from '../catalog-C4Gv2_KM.js';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-uteRCL9q.js';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -10,10 +10,10 @@ import {
10
10
  SUPER_ADMIN_NOTIFY_KEY,
11
11
  buildNavigationGuard,
12
12
  createSuperAdminI18n
13
- } from "../chunk-TY2WYKME.js";
13
+ } from "../chunk-247RJPU4.js";
14
14
  import {
15
15
  defaultHttpClient
16
- } from "../chunk-TBLXKSTR.js";
16
+ } from "../chunk-IZGFS5RS.js";
17
17
 
18
18
  // src/quasar/create-super-admin-app.ts
19
19
  import { createApp } from "vue";
@@ -1,7 +1,7 @@
1
1
  import { Component, App, InjectionKey, Ref, ComputedRef } from 'vue';
2
2
  import { NavigationGuardWithThis } from 'vue-router';
3
3
  import { ActionKey, ComponentKey, AdminManifest } from '@saasicat/types';
4
- import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides } from './messages-BIYw32u1.cjs';
4
+ import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaLocaleOption, c as SaLocaleDefinition, d as SaMessagesOverrides, K as KvStore } from './catalog-C4Gv2_KM.cjs';
5
5
 
6
6
  /**
7
7
  * App-specific branding data that the platform `AdminLayout` and other
@@ -182,16 +182,62 @@ interface SuperAdminI18n {
182
182
  messages: ComputedRef<SaMessages>;
183
183
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
184
184
  intlLocale: ComputedRef<string>;
185
+ /**
186
+ * Whether the shell renders its language switcher. `false` when the app
187
+ * opted out, when `locale` cannot be written to, or when only one language
188
+ * is on offer — a switcher that cannot change anything is worse than none.
189
+ */
190
+ switcherEnabled: boolean;
191
+ /** Locales the switcher offers, in order, with their display names. */
192
+ availableLocales: readonly SaLocaleOption[];
185
193
  }
186
194
  interface SuperAdminI18nOptions {
187
195
  /**
188
- * Initial UI locale, default `'de'`. Pass a `Ref` when the app wants to
189
- * keep control (e.g. a user-profile setting or a header switcher).
196
+ * Starting UI locale — the shell's switcher lets the user pick another one,
197
+ * and a stored pick outranks this. Defaults to `'de'` when offered,
198
+ * otherwise to the first entry of `locales`. Pass a `Ref` when the app wants
199
+ * to keep control (e.g. a user-profile setting).
190
200
  */
191
201
  locale?: SaLocale | Ref<SaLocale>;
192
- /** Per-locale string overrides layered over the platform catalog. */
202
+ /**
203
+ * Languages to offer, in switcher order. Default: German, English and
204
+ * everything in `additionalLocales`. Narrow it to ship a single language —
205
+ * the switcher then hides itself.
206
+ */
207
+ locales?: readonly SaLocale[];
208
+ /**
209
+ * Languages the app adds on top of the two the platform ships, keyed by
210
+ * locale code. Untranslated keys fall back to `basedOn`, so a partial
211
+ * catalog is usable from the first key onwards.
212
+ */
213
+ additionalLocales?: Readonly<Record<string, SaLocaleDefinition>>;
214
+ /** Per-locale string overrides layered over whatever catalog applies. */
193
215
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
216
+ /**
217
+ * Remembers the locale picked in the header switcher across reloads,
218
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
219
+ * and persists it wherever it belongs (user profile, its own store).
220
+ */
221
+ persist?: boolean;
222
+ /**
223
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`.
224
+ */
225
+ storage?: KvStore;
226
+ /**
227
+ * Prefix for the persisted key, mirroring `createPlatformLoaders`. Set it
228
+ * when several apps share one origin and must not inherit each other's
229
+ * language pick.
230
+ */
231
+ storageKeyPrefix?: string;
232
+ /**
233
+ * Renders the language switcher in the shell chrome, default `true`. Set
234
+ * `false` for a deployment that ships one language. A readonly `locale`
235
+ * ref disables it on its own — no need to set this as well.
236
+ */
237
+ switcher?: boolean;
194
238
  }
239
+ /** `KvStore` key holding the locale the user picked in the switcher. */
240
+ declare const SA_LOCALE_STORAGE_KEY = "sa:locale";
195
241
  declare const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n>;
196
242
  declare function createSuperAdminI18n(options?: SuperAdminI18nOptions): SuperAdminI18n;
197
243
  /**
@@ -207,4 +253,4 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
207
253
  */
208
254
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
209
255
 
210
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SUPER_ADMIN_ACTIONS_KEY as g, SUPER_ADMIN_BRAND_KEY as h, SUPER_ADMIN_ENDPOINTS_KEY as i, SUPER_ADMIN_EXTENSIONS_KEY as j, SUPER_ADMIN_HTTP_KEY as k, SUPER_ADMIN_I18N_KEY as l, SUPER_ADMIN_LOGIN_ADAPTER_KEY as m, SUPER_ADMIN_MANIFEST_KEY as n, SUPER_ADMIN_NOTIFY_KEY as o, type SuperAdminAuthGuardOptions as p, type SuperAdminLoginResult as q, type SuperAdminManifestGuardOptions as r, type UiNotifyKind as s, type UiNotifyOptions as t, buildNavigationGuard as u, createSuperAdminI18n as v, useSaMessages as w, useSuperAdminI18n as x };
256
+ export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_KEY as o, SUPER_ADMIN_NOTIFY_KEY as p, type SuperAdminAuthGuardOptions as q, type SuperAdminLoginResult as r, type SuperAdminManifestGuardOptions as s, type UiNotifyKind as t, type UiNotifyOptions as u, buildNavigationGuard as v, createSuperAdminI18n as w, useSaMessages as x, useSuperAdminI18n as y };
@@ -1,7 +1,7 @@
1
1
  import { Component, App, InjectionKey, Ref, ComputedRef } from 'vue';
2
2
  import { NavigationGuardWithThis } from 'vue-router';
3
3
  import { ActionKey, ComponentKey, AdminManifest } from '@saasicat/types';
4
- import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides } from './messages-BIYw32u1.js';
4
+ import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaLocaleOption, c as SaLocaleDefinition, d as SaMessagesOverrides, K as KvStore } from './catalog-C4Gv2_KM.js';
5
5
 
6
6
  /**
7
7
  * App-specific branding data that the platform `AdminLayout` and other
@@ -182,16 +182,62 @@ interface SuperAdminI18n {
182
182
  messages: ComputedRef<SaMessages>;
183
183
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
184
184
  intlLocale: ComputedRef<string>;
185
+ /**
186
+ * Whether the shell renders its language switcher. `false` when the app
187
+ * opted out, when `locale` cannot be written to, or when only one language
188
+ * is on offer — a switcher that cannot change anything is worse than none.
189
+ */
190
+ switcherEnabled: boolean;
191
+ /** Locales the switcher offers, in order, with their display names. */
192
+ availableLocales: readonly SaLocaleOption[];
185
193
  }
186
194
  interface SuperAdminI18nOptions {
187
195
  /**
188
- * Initial UI locale, default `'de'`. Pass a `Ref` when the app wants to
189
- * keep control (e.g. a user-profile setting or a header switcher).
196
+ * Starting UI locale — the shell's switcher lets the user pick another one,
197
+ * and a stored pick outranks this. Defaults to `'de'` when offered,
198
+ * otherwise to the first entry of `locales`. Pass a `Ref` when the app wants
199
+ * to keep control (e.g. a user-profile setting).
190
200
  */
191
201
  locale?: SaLocale | Ref<SaLocale>;
192
- /** Per-locale string overrides layered over the platform catalog. */
202
+ /**
203
+ * Languages to offer, in switcher order. Default: German, English and
204
+ * everything in `additionalLocales`. Narrow it to ship a single language —
205
+ * the switcher then hides itself.
206
+ */
207
+ locales?: readonly SaLocale[];
208
+ /**
209
+ * Languages the app adds on top of the two the platform ships, keyed by
210
+ * locale code. Untranslated keys fall back to `basedOn`, so a partial
211
+ * catalog is usable from the first key onwards.
212
+ */
213
+ additionalLocales?: Readonly<Record<string, SaLocaleDefinition>>;
214
+ /** Per-locale string overrides layered over whatever catalog applies. */
193
215
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
216
+ /**
217
+ * Remembers the locale picked in the header switcher across reloads,
218
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
219
+ * and persists it wherever it belongs (user profile, its own store).
220
+ */
221
+ persist?: boolean;
222
+ /**
223
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`.
224
+ */
225
+ storage?: KvStore;
226
+ /**
227
+ * Prefix for the persisted key, mirroring `createPlatformLoaders`. Set it
228
+ * when several apps share one origin and must not inherit each other's
229
+ * language pick.
230
+ */
231
+ storageKeyPrefix?: string;
232
+ /**
233
+ * Renders the language switcher in the shell chrome, default `true`. Set
234
+ * `false` for a deployment that ships one language. A readonly `locale`
235
+ * ref disables it on its own — no need to set this as well.
236
+ */
237
+ switcher?: boolean;
194
238
  }
239
+ /** `KvStore` key holding the locale the user picked in the switcher. */
240
+ declare const SA_LOCALE_STORAGE_KEY = "sa:locale";
195
241
  declare const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n>;
196
242
  declare function createSuperAdminI18n(options?: SuperAdminI18nOptions): SuperAdminI18n;
197
243
  /**
@@ -207,4 +253,4 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
207
253
  */
208
254
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
209
255
 
210
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SUPER_ADMIN_ACTIONS_KEY as g, SUPER_ADMIN_BRAND_KEY as h, SUPER_ADMIN_ENDPOINTS_KEY as i, SUPER_ADMIN_EXTENSIONS_KEY as j, SUPER_ADMIN_HTTP_KEY as k, SUPER_ADMIN_I18N_KEY as l, SUPER_ADMIN_LOGIN_ADAPTER_KEY as m, SUPER_ADMIN_MANIFEST_KEY as n, SUPER_ADMIN_NOTIFY_KEY as o, type SuperAdminAuthGuardOptions as p, type SuperAdminLoginResult as q, type SuperAdminManifestGuardOptions as r, type UiNotifyKind as s, type UiNotifyOptions as t, buildNavigationGuard as u, createSuperAdminI18n as v, useSaMessages as w, useSuperAdminI18n as x };
256
+ export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_KEY as o, SUPER_ADMIN_NOTIFY_KEY as p, type SuperAdminAuthGuardOptions as q, type SuperAdminLoginResult as r, type SuperAdminManifestGuardOptions as s, type UiNotifyKind as t, type UiNotifyOptions as u, buildNavigationGuard as v, createSuperAdminI18n as w, useSaMessages as x, useSuperAdminI18n as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/ui-vue",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Vue 3 components, resource clients and composables for the SuperAdmin UI shell. Provides boot and manifest loaders, navigation, actions and standard pages.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -53,7 +53,7 @@
53
53
  "src"
54
54
  ],
55
55
  "dependencies": {
56
- "@saasicat/types": "^0.15.1"
56
+ "@saasicat/types": "^0.17.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "pinia": "^2.0.0 || ^3.0.0",
@@ -75,10 +75,14 @@
75
75
  "devDependencies": {
76
76
  "@playwright/test": "^1.59.1",
77
77
  "@types/node": "^25.6.0",
78
+ "@vitejs/plugin-vue": "^6.0.8",
79
+ "@vue/test-utils": "^2.4.11",
80
+ "jsdom": "^30.0.1",
78
81
  "pinia": "^3.0.0",
79
- "quasar": "^2.22.0",
82
+ "quasar": "^2.24.0",
80
83
  "tsup": "^8.0.0",
81
84
  "typescript": "^6.0.0",
85
+ "vitest": "^4.1.10",
82
86
  "vue": "^3.5.0",
83
87
  "vue-router": "^5.0.0",
84
88
  "vue-tsc": "^3.3.3"
@@ -102,6 +106,7 @@
102
106
  "typecheck": "vue-tsc --noEmit -p tsconfig.json",
103
107
  "pretest": "pnpm run build",
104
108
  "test": "node --test tests/**/*.test.js",
109
+ "test:component": "vitest run --config vitest.config.ts",
105
110
  "test:e2e": "playwright test"
106
111
  }
107
112
  }
@@ -0,0 +1,120 @@
1
+ // Resolves a locale to the three things the shell needs from it: the catalog to
2
+ // render, the tag `Intl` formats with, and the name the switcher shows.
3
+ //
4
+ // The platform ships German and English and stops there. Which of them an app
5
+ // offers, which languages it adds, and what wording it prefers is the app's
6
+ // decision — this module is the single place all three are applied.
7
+
8
+ import { mergeMessages } from './define.js';
9
+ import {
10
+ DEFAULT_SA_LOCALE,
11
+ SA_INTL_LOCALES,
12
+ SA_LOCALES,
13
+ SA_LOCALE_LABELS,
14
+ isSaBuiltinLocale,
15
+ type SaBuiltinLocale,
16
+ type SaLocale,
17
+ } from './locale.js';
18
+ import { SA_MESSAGES, type SaMessages, type SaMessagesOverrides } from './messages.js';
19
+
20
+ /** A language an app brings along, on top of the two the platform ships. */
21
+ export interface SaLocaleDefinition {
22
+ /**
23
+ * Name for the switcher, written in the language itself (`'Français'`) —
24
+ * that way the entry stays readable whatever locale is currently active.
25
+ */
26
+ label: string;
27
+ /** BCP-47 tag for `Intl`/`toLocaleString`, e.g. `'fr-FR'`. */
28
+ intlLocale: string;
29
+ /**
30
+ * Built-in catalog that fills whatever `messages` leaves out, default
31
+ * `'en'`. A partial translation therefore renders — untranslated keys fall
32
+ * back instead of showing blanks.
33
+ */
34
+ basedOn?: SaBuiltinLocale;
35
+ /** The translation. Every key is optional; see `basedOn`. */
36
+ messages: SaMessagesOverrides;
37
+ }
38
+
39
+ export interface SaCatalogOptions {
40
+ /**
41
+ * Locales offered in the switcher, in the order given. Default: the
42
+ * built-ins plus everything in `additionalLocales`. Narrow it to ship a
43
+ * single language; entries without a catalog are ignored.
44
+ */
45
+ locales?: readonly SaLocale[];
46
+ /** Languages the app adds, keyed by locale code (`'fr'`, `'pt-BR'`, …). */
47
+ additionalLocales?: Readonly<Record<string, SaLocaleDefinition>>;
48
+ /** Per-locale wording changes layered on top of whatever catalog applies. */
49
+ overrides?: Readonly<Partial<Record<SaLocale, SaMessagesOverrides>>>;
50
+ }
51
+
52
+ /** One entry of the switcher. */
53
+ export interface SaLocaleOption {
54
+ code: SaLocale;
55
+ label: string;
56
+ }
57
+
58
+ export interface SaCatalog {
59
+ /** Locales to offer, in switcher order. Never empty. */
60
+ readonly locales: readonly SaLocaleOption[];
61
+ /**
62
+ * Whether the app offers this locale. Deliberately not "can it be
63
+ * rendered": an app that narrowed to English must not be pushed back into
64
+ * German by a stored pick from before, even though German is still shipped.
65
+ */
66
+ has(locale: SaLocale): boolean;
67
+ /** Locale to start from when nothing else is known. */
68
+ readonly defaultLocale: SaLocale;
69
+ messagesFor(locale: SaLocale): SaMessages;
70
+ intlLocaleFor(locale: SaLocale): string;
71
+ labelFor(locale: SaLocale): string;
72
+ }
73
+
74
+ /**
75
+ * Builds the catalog an app runs on. Resolution is cached per locale, so
76
+ * switching back and forth does not re-merge the message tree.
77
+ */
78
+ export function createSaCatalog(options: SaCatalogOptions = {}): SaCatalog {
79
+ const additional = options.additionalLocales ?? {};
80
+ const known = new Set<string>([...SA_LOCALES, ...Object.keys(additional)]);
81
+
82
+ // An app that lists an unknown code would otherwise get a switcher entry
83
+ // that renders nothing; dropping it here keeps the failure visible in one
84
+ // place instead of at every call site.
85
+ const requested = options.locales?.filter((code) => known.has(code));
86
+ const offered: readonly SaLocale[] =
87
+ requested && requested.length > 0 ? requested : [...SA_LOCALES, ...Object.keys(additional)];
88
+
89
+ const labelFor = (locale: SaLocale): string =>
90
+ additional[locale]?.label ??
91
+ (isSaBuiltinLocale(locale) ? SA_LOCALE_LABELS[locale] : locale);
92
+
93
+ const cache = new Map<SaLocale, SaMessages>();
94
+ const messagesFor = (locale: SaLocale): SaMessages => {
95
+ const hit = cache.get(locale);
96
+ if (hit) return hit;
97
+ const definition = additional[locale];
98
+ const base =
99
+ SA_MESSAGES[definition?.basedOn ?? (isSaBuiltinLocale(locale) ? locale : 'en')];
100
+ const translated = definition ? mergeMessages(base, definition.messages) : base;
101
+ const resolved = mergeMessages(translated, options.overrides?.[locale]);
102
+ cache.set(locale, resolved);
103
+ return resolved;
104
+ };
105
+
106
+ const offeredSet = new Set<string>(offered);
107
+
108
+ return {
109
+ locales: offered.map((code) => ({ code, label: labelFor(code) })),
110
+ has: (locale) => offeredSet.has(locale),
111
+ // The app's first offered locale beats the platform default: an app
112
+ // shipping English only must not start in German.
113
+ defaultLocale: offered.includes(DEFAULT_SA_LOCALE) ? DEFAULT_SA_LOCALE : offered[0],
114
+ messagesFor,
115
+ intlLocaleFor: (locale) =>
116
+ additional[locale]?.intlLocale ??
117
+ (isSaBuiltinLocale(locale) ? SA_INTL_LOCALES[locale] : locale),
118
+ labelFor,
119
+ };
120
+ }
@@ -2,7 +2,12 @@
2
2
  // grouping and decimal separator follow the active UI locale
3
3
  // (de → `12.345,60 €`, en → `€12,345.60`).
4
4
 
5
- import { DEFAULT_SA_LOCALE, SA_INTL_LOCALES, type SaLocale } from './locale.js';
5
+ import {
6
+ DEFAULT_SA_LOCALE,
7
+ SA_INTL_LOCALES,
8
+ type SaBuiltinLocale,
9
+ type SaLocale,
10
+ } from './locale.js';
6
11
 
7
12
  const DEFAULT_CURRENCY = 'EUR';
8
13
 
@@ -12,7 +17,7 @@ function formatterFor(locale: SaLocale, currency: string, decimals: number): Int
12
17
  const key = `${locale}:${currency}:${decimals}`;
13
18
  let formatter = formatters.get(key);
14
19
  if (!formatter) {
15
- formatter = new Intl.NumberFormat(SA_INTL_LOCALES[locale], {
20
+ formatter = new Intl.NumberFormat(SA_INTL_LOCALES[locale as SaBuiltinLocale] ?? locale, {
16
21
  style: 'currency',
17
22
  currency,
18
23
  minimumFractionDigits: decimals,