@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
@@ -7,6 +7,7 @@
7
7
  <div class="sa-setup-title">{{ msg.setup.title }}</div>
8
8
  <div class="sa-setup-sub">{{ subtitleText }}</div>
9
9
  </div>
10
+ <LocaleSwitcher class="sa-setup-locale" />
10
11
  </div>
11
12
 
12
13
  <q-banner v-if="errorMessage" class="sa-setup-error" rounded>
@@ -160,6 +161,7 @@ import {
160
161
  import { HttpJsonError, postJson as httpPostJson } from '../client/http-json.js';
161
162
  import { formatMessage } from '../client/i18n/format.js';
162
163
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
164
+ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
163
165
 
164
166
  interface Props {
165
167
  /** Display name + badge abbreviation (override the app branding, e.g. from PublicBoot). */
@@ -288,6 +290,11 @@ async function submitConfirm(): Promise<void> {
288
290
  gap: 12px;
289
291
  margin-bottom: 20px;
290
292
  }
293
+ .sa-setup-locale {
294
+ margin-left: auto;
295
+ align-self: flex-start;
296
+ color: #475569;
297
+ }
291
298
  .sa-setup-badge {
292
299
  width: 44px;
293
300
  height: 44px;
@@ -4,7 +4,7 @@
4
4
  // labels/colors/coverage calculations.
5
5
 
6
6
  import type { CatalogEntryI18n, DiscoveryStatus } from '@saasicat/types';
7
- import { DEFAULT_SA_LOCALE, type SaLocale } from '../../client/i18n/locale.js';
7
+ import { DEFAULT_SA_LOCALE, builtinLocaleOf, type SaLocale } from '../../client/i18n/locale.js';
8
8
  import { commonMessages } from '../../client/i18n/messages/common.js';
9
9
  import { discoveryMessages } from '../../client/i18n/messages/discovery.js';
10
10
 
@@ -43,19 +43,19 @@ export function localeFull(locale: string): string {
43
43
  }
44
44
 
45
45
  export function i18nFieldLabel(f: I18nField, locale: SaLocale = DEFAULT_SA_LOCALE): string {
46
- if (f === 'label') return discoveryMessages[locale].trans.fieldLabel;
47
- if (f === 'description') return commonMessages[locale].description;
48
- return discoveryMessages[locale].unit;
46
+ if (f === 'label') return discoveryMessages[builtinLocaleOf(locale)].trans.fieldLabel;
47
+ if (f === 'description') return commonMessages[builtinLocaleOf(locale)].description;
48
+ return discoveryMessages[builtinLocaleOf(locale)].unit;
49
49
  }
50
50
 
51
51
  // ─── Approval lifecycle (#20): status display + state machine ────────────────
52
52
 
53
53
  export function statusLabel(status: DiscoveryStatus, locale: SaLocale = DEFAULT_SA_LOCALE): string {
54
- return discoveryMessages[locale].statusLabels[status];
54
+ return discoveryMessages[builtinLocaleOf(locale)].statusLabels[status];
55
55
  }
56
56
 
57
57
  export function statusHint(status: DiscoveryStatus, locale: SaLocale = DEFAULT_SA_LOCALE): string {
58
- return discoveryMessages[locale].statusHints[status];
58
+ return discoveryMessages[builtinLocaleOf(locale)].statusHints[status];
59
59
  }
60
60
 
61
61
  export interface ReviewAction {
@@ -71,7 +71,7 @@ export function primaryReviewAction(
71
71
  status: DiscoveryStatus,
72
72
  locale: SaLocale = DEFAULT_SA_LOCALE,
73
73
  ): ReviewAction {
74
- const actions = discoveryMessages[locale].reviewActions;
74
+ const actions = discoveryMessages[builtinLocaleOf(locale)].reviewActions;
75
75
  switch (status) {
76
76
  case 'pending':
77
77
  return { label: actions.approve, to: 'approved', emphasized: true };
@@ -89,7 +89,7 @@ export function reviewMenuActions(
89
89
  status: DiscoveryStatus,
90
90
  locale: SaLocale = DEFAULT_SA_LOCALE,
91
91
  ): ReviewAction[] {
92
- const actions = discoveryMessages[locale].reviewActions;
92
+ const actions = discoveryMessages[builtinLocaleOf(locale)].reviewActions;
93
93
  const markObsolete: ReviewAction = {
94
94
  label: actions.markObsolete,
95
95
  to: 'obsolete',
@@ -1,13 +1,13 @@
1
1
  // Formatting helper retained for the reusable catalog-history timeline.
2
2
 
3
3
  import { formatMessage } from '../../client/i18n/format.js';
4
- import { DEFAULT_SA_LOCALE, type SaLocale } from '../../client/i18n/locale.js';
4
+ import { DEFAULT_SA_LOCALE, builtinLocaleOf, type SaLocale } from '../../client/i18n/locale.js';
5
5
  import { planVersionsMessages } from '../../client/i18n/messages/plan-versions.js';
6
6
 
7
7
  const EMPTY_VALUE = '—';
8
8
 
9
9
  function texts(locale: SaLocale) {
10
- return planVersionsMessages[locale].format;
10
+ return planVersionsMessages[builtinLocaleOf(locale)].format;
11
11
  }
12
12
 
13
13
  export function formatRelative(
@@ -2,22 +2,35 @@
2
2
  // provides one instance; pages, components and composables read it via
3
3
  // `useSuperAdminI18n()` or the namespace-focused `useSaMessages()`.
4
4
  //
5
- // Design: no vue-i18n dependency. The platform ships complete typed catalogs
6
- // (`SA_MESSAGES`, German reference + English translation) and consumers switch
7
- // the locale or override individual strings through the bootstrap option.
8
- // Without a provider the composables fall back to a shared German instance, so
5
+ // Design: no vue-i18n dependency. The platform ships two complete typed
6
+ // catalogs (German reference + English translation) and nothing beyond that —
7
+ // which of them an app offers, which languages it adds and what wording it
8
+ // prefers is decided through the bootstrap option and resolved in `SaCatalog`.
9
+ // Without a provider the composables fall back to a shared default instance, so
9
10
  // isolated mounts and unit tests need no setup.
10
11
 
11
- import { computed, inject, isRef, ref, type ComputedRef, type InjectionKey, type Ref } from 'vue';
12
+ import {
13
+ computed,
14
+ inject,
15
+ isReadonly,
16
+ isRef,
17
+ ref,
18
+ watch,
19
+ type ComputedRef,
20
+ type InjectionKey,
21
+ type Ref,
22
+ } from 'vue';
12
23
 
13
24
  import {
14
- DEFAULT_SA_LOCALE,
15
- SA_INTL_LOCALES,
16
- resolveMessages,
25
+ createSaCatalog,
26
+ type SaCatalog,
17
27
  type SaLocale,
28
+ type SaLocaleDefinition,
29
+ type SaLocaleOption,
18
30
  type SaMessages,
19
31
  type SaMessagesOverrides,
20
32
  } from '../client/i18n/index.js';
33
+ import { defaultKvStore, type KvStore } from '../client/types.js';
21
34
 
22
35
  export interface SuperAdminI18n {
23
36
  /** Active UI locale — mutable; switching re-renders all catalog texts. */
@@ -26,18 +39,65 @@ export interface SuperAdminI18n {
26
39
  messages: ComputedRef<SaMessages>;
27
40
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
28
41
  intlLocale: ComputedRef<string>;
42
+ /**
43
+ * Whether the shell renders its language switcher. `false` when the app
44
+ * opted out, when `locale` cannot be written to, or when only one language
45
+ * is on offer — a switcher that cannot change anything is worse than none.
46
+ */
47
+ switcherEnabled: boolean;
48
+ /** Locales the switcher offers, in order, with their display names. */
49
+ availableLocales: readonly SaLocaleOption[];
29
50
  }
30
51
 
31
52
  export interface SuperAdminI18nOptions {
32
53
  /**
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).
54
+ * Starting UI locale — the shell's switcher lets the user pick another one,
55
+ * and a stored pick outranks this. Defaults to `'de'` when offered,
56
+ * otherwise to the first entry of `locales`. Pass a `Ref` when the app wants
57
+ * to keep control (e.g. a user-profile setting).
35
58
  */
36
59
  locale?: SaLocale | Ref<SaLocale>;
37
- /** Per-locale string overrides layered over the platform catalog. */
60
+ /**
61
+ * Languages to offer, in switcher order. Default: German, English and
62
+ * everything in `additionalLocales`. Narrow it to ship a single language —
63
+ * the switcher then hides itself.
64
+ */
65
+ locales?: readonly SaLocale[];
66
+ /**
67
+ * Languages the app adds on top of the two the platform ships, keyed by
68
+ * locale code. Untranslated keys fall back to `basedOn`, so a partial
69
+ * catalog is usable from the first key onwards.
70
+ */
71
+ additionalLocales?: Readonly<Record<string, SaLocaleDefinition>>;
72
+ /** Per-locale string overrides layered over whatever catalog applies. */
38
73
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
74
+ /**
75
+ * Remembers the locale picked in the header switcher across reloads,
76
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
77
+ * and persists it wherever it belongs (user profile, its own store).
78
+ */
79
+ persist?: boolean;
80
+ /**
81
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`.
82
+ */
83
+ storage?: KvStore;
84
+ /**
85
+ * Prefix for the persisted key, mirroring `createPlatformLoaders`. Set it
86
+ * when several apps share one origin and must not inherit each other's
87
+ * language pick.
88
+ */
89
+ storageKeyPrefix?: string;
90
+ /**
91
+ * Renders the language switcher in the shell chrome, default `true`. Set
92
+ * `false` for a deployment that ships one language. A readonly `locale`
93
+ * ref disables it on its own — no need to set this as well.
94
+ */
95
+ switcher?: boolean;
39
96
  }
40
97
 
98
+ /** `KvStore` key holding the locale the user picked in the switcher. */
99
+ export const SA_LOCALE_STORAGE_KEY = 'sa:locale';
100
+
41
101
  // `Symbol.for` — see super-admin-context.ts: dist- and src-imported module
42
102
  // instances must resolve to the same injection key.
43
103
  export const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n> = Symbol.for(
@@ -45,14 +105,48 @@ export const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n> = Symbol.for(
45
105
  );
46
106
 
47
107
  export function createSuperAdminI18n(options: SuperAdminI18nOptions = {}): SuperAdminI18n {
48
- const locale = isRef(options.locale)
49
- ? options.locale
50
- : ref(options.locale ?? DEFAULT_SA_LOCALE);
51
- const messages = computed(() =>
52
- resolveMessages(locale.value, options.overrides?.[locale.value]),
108
+ const catalog = createSaCatalog(options);
109
+ const locale = isRef(options.locale) ? options.locale : createOwnedLocale(options, catalog);
110
+ // An unknown locale would otherwise render an empty shell. Falling back
111
+ // keeps the UI usable and makes the app's mistake visible as "wrong
112
+ // language" rather than "blank page".
113
+ const active = computed(() =>
114
+ catalog.has(locale.value) ? locale.value : catalog.defaultLocale,
53
115
  );
54
- const intlLocale = computed(() => SA_INTL_LOCALES[locale.value]);
55
- return { locale, messages, intlLocale };
116
+ const messages = computed(() => catalog.messagesFor(active.value));
117
+ const intlLocale = computed(() => catalog.intlLocaleFor(active.value));
118
+ // A readonly ref (typically a `computed` derived from a profile setting)
119
+ // silently swallows writes, so the switcher would be a dead control. The
120
+ // type system permits it — TypeScript ignores `readonly` when checking
121
+ // assignability — so the guard has to be a runtime one.
122
+ const writable = !isReadonly(locale);
123
+ return {
124
+ locale,
125
+ messages,
126
+ intlLocale,
127
+ switcherEnabled: (options.switcher ?? true) && writable && catalog.locales.length > 1,
128
+ availableLocales: catalog.locales,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Builds the locale ref the platform owns. A stored choice outranks the
134
+ * `locale` option, because that option is the app's default while the stored
135
+ * value is the user's explicit pick in the switcher.
136
+ */
137
+ function createOwnedLocale(options: SuperAdminI18nOptions, catalog: SaCatalog): Ref<SaLocale> {
138
+ const initial = options.locale ?? catalog.defaultLocale;
139
+ if (options.persist === false) return ref(initial);
140
+
141
+ const storage = options.storage ?? defaultKvStore();
142
+ const key = `${options.storageKeyPrefix ?? ''}${SA_LOCALE_STORAGE_KEY}`;
143
+ const stored = storage.get(key);
144
+ // The stored value is checked against what this app offers, not against the
145
+ // built-ins: an app that dropped a language must not be pushed back into it
146
+ // by a pick from before.
147
+ const locale = ref(stored && catalog.has(stored) ? stored : initial);
148
+ watch(locale, (next) => storage.set(key, next));
149
+ return locale;
56
150
  }
57
151
 
58
152
  let fallbackI18n: SuperAdminI18n | null = null;
@@ -65,7 +159,10 @@ let fallbackI18n: SuperAdminI18n | null = null;
65
159
  export function useSuperAdminI18n(): SuperAdminI18n {
66
160
  const injected = inject(SUPER_ADMIN_I18N_KEY, null);
67
161
  if (injected) return injected;
68
- fallbackI18n ??= createSuperAdminI18n();
162
+ // Deliberately not persisted: this instance exists for mounts outside a
163
+ // shell, and a shared singleton that reads and writes real storage would
164
+ // make test outcomes depend on their order.
165
+ fallbackI18n ??= createSuperAdminI18n({ persist: false });
69
166
  return fallbackI18n;
70
167
  }
71
168