@saasicat/ui-vue 0.16.0 → 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 (40) hide show
  1. package/README.md +12 -11
  2. package/dist/{messages-DetB75lX.d.ts → catalog-C4Gv2_KM.d.cts} +115 -40
  3. package/dist/{messages-DetB75lX.d.cts → catalog-C4Gv2_KM.d.ts} +115 -40
  4. package/dist/{chunk-MGKJCVDU.js → chunk-247RJPU4.js} +18 -18
  5. package/dist/{chunk-TZ2NGQXK.js → chunk-IZGFS5RS.js} +44 -8
  6. package/dist/{chunk-CRUUZMMN.js → chunk-XPSD5HKG.js} +30 -4
  7. package/dist/client/index.cjs +75 -10
  8. package/dist/client/index.d.cts +51 -5
  9. package/dist/client/index.d.ts +51 -5
  10. package/dist/client/index.js +12 -4
  11. package/dist/index.cjs +90 -22
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +13 -5
  15. package/dist/quasar/index.cjs +54 -20
  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-DokHhkS_.d.cts → use-super-admin-i18n-BdPqeTHl.d.cts} +29 -9
  20. package/dist/{use-super-admin-i18n-BPC-H9jz.d.ts → use-super-admin-i18n-uteRCL9q.d.ts} +29 -9
  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 +40 -11
  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.ts +7 -4
  31. package/src/client/index.ts +1 -0
  32. package/src/client/login-branding.ts +61 -0
  33. package/src/client/nav-builder.ts +33 -6
  34. package/src/components/LocaleSwitcher.vue +21 -12
  35. package/src/components/bundle-editor/bundle-version-status.ts +16 -8
  36. package/src/pages-standard/AdminLayout.vue +7 -3
  37. package/src/pages-standard/SuperAdminLoginPage.vue +8 -9
  38. package/src/pages-standard/discovery-page/discovery-ui.ts +8 -8
  39. package/src/pages-standard/plan-versions/format.ts +2 -2
  40. package/src/vue/use-super-admin-i18n.ts +59 -28
@@ -6,7 +6,12 @@
6
6
 
7
7
  import type { BundleVersionRow, PlanVersionRow } from '@saasicat/types';
8
8
 
9
- import { SA_INTL_LOCALES, type SaLocale } from '../../client/i18n/locale.js';
9
+ import {
10
+ SA_INTL_LOCALES,
11
+ builtinLocaleOf,
12
+ type SaBuiltinLocale,
13
+ type SaLocale,
14
+ } from '../../client/i18n/locale.js';
10
15
  import { bundlesMessages } from '../../client/i18n/messages/bundles.js';
11
16
 
12
17
  /** UI lifecycle status of a single BundleVersion. */
@@ -67,7 +72,7 @@ export function bundleStatusMeta(
67
72
  status: BundleAggregateStatus,
68
73
  locale: SaLocale,
69
74
  ): BundleStatusMeta {
70
- const texts = bundlesMessages[locale].status[status];
75
+ const texts = bundlesMessages[builtinLocaleOf(locale)].status[status];
71
76
  return { label: texts.label, cls: BUNDLE_STATUS_CLASS[status], tooltip: texts.tooltip };
72
77
  }
73
78
 
@@ -155,10 +160,13 @@ export function formatDate(iso: string | null | undefined, locale: SaLocale): st
155
160
  const day = iso.slice(0, 10);
156
161
  const [y, m, d] = day.split('-');
157
162
  if (!y || !m || !d) return iso;
158
- return new Date(`${day}T00:00:00Z`).toLocaleDateString(SA_INTL_LOCALES[locale], {
159
- day: '2-digit',
160
- month: '2-digit',
161
- year: 'numeric',
162
- timeZone: 'UTC',
163
- });
163
+ return new Date(`${day}T00:00:00Z`).toLocaleDateString(
164
+ SA_INTL_LOCALES[locale as SaBuiltinLocale] ?? locale,
165
+ {
166
+ day: '2-digit',
167
+ month: '2-digit',
168
+ year: 'numeric',
169
+ timeZone: 'UTC',
170
+ },
171
+ );
164
172
  }
@@ -189,7 +189,7 @@ const emit = defineEmits<{
189
189
 
190
190
  const route = useRoute();
191
191
  const msg = useSaMessages('shell');
192
- const { locale } = useSuperAdminI18n();
192
+ const { messages } = useSuperAdminI18n();
193
193
  // Provided by createSuperAdminApp(); null when the layout is mounted
194
194
  // stand-alone. Grouped into one object so the names cannot shadow the
195
195
  // same-named props inside the template.
@@ -227,13 +227,17 @@ const navSections = computed<NavSection[]>(() => {
227
227
  // section, so that the UI has no flicker before manifest load.
228
228
  return [{ title: null, items: [...props.staticNavFallback, ...props.localItems] }];
229
229
  }
230
+ // The resolved catalog rather than the bare locale: it carries
231
+ // `i18n.overrides` and languages the app added itself, neither of which the
232
+ // builder can look up from a locale code.
233
+ const nav = messages.value.nav;
230
234
  const routes = buildRoutes(m, {
231
- locale: locale.value,
235
+ nav,
232
236
  standardPageRoutes: props.standardPageRoutes,
233
237
  standardPageNavSection: props.standardPageNavSection,
234
238
  availableExtensions: props.availableExtensions,
235
239
  });
236
- const sections = buildSidebar(routes, props.sectionOrder ?? defaultSectionOrder(locale.value));
240
+ const sections = buildSidebar(routes, props.sectionOrder ?? defaultSectionOrder(nav));
237
241
  const result: NavSection[] = sections.map((s) => ({
238
242
  title: s.section,
239
243
  items: s.items.map((item) => ({
@@ -98,6 +98,7 @@ import {
98
98
  useSuperAdminLoginAdapter,
99
99
  } from '../vue/use-super-admin-context.js';
100
100
  import { getJson } from '../client/http-json.js';
101
+ import { isProductionBoot, resolveLoginBranding } from '../client/login-branding.js';
101
102
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
102
103
  import LocaleSwitcher from '../components/LocaleSwitcher.vue';
103
104
  import SuperAdminSetupWizard from './SuperAdminSetupWizard.vue';
@@ -128,18 +129,16 @@ const errorMessage = ref<string | null>(null);
128
129
  // wizard instead of the login. Apps without a SetupModule return 404 → stays false.
129
130
  const needsSetup = ref(false);
130
131
 
131
- const brandName = computed(() => boot.boot.value?.project.displayName ?? brand.name);
132
- const tagText = computed(() => boot.boot.value?.project.label ?? brand.tag ?? 'SuperAdmin');
133
- const iconText = computed(() => boot.boot.value?.project.icon ?? brand.logoText);
134
- const logoUrl = computed(() => boot.boot.value?.project.logoUrl ?? null);
135
- const bootEnvironment = computed(() => {
136
- const env = boot.boot.value?.project.environment;
137
- return env && env !== 'production' ? env : null;
138
- });
132
+ const branding = computed(() => resolveLoginBranding(boot.boot.value, brand));
133
+ const brandName = computed(() => branding.value.name);
134
+ const tagText = computed(() => branding.value.tag);
135
+ const iconText = computed(() => branding.value.icon);
136
+ const logoUrl = computed(() => branding.value.logoUrl);
137
+ const bootEnvironment = computed(() => branding.value.environment);
139
138
 
140
139
  const devHint = computed(() => {
141
140
  if (!adapter.devHint) return null;
142
- if (boot.boot.value?.project.environment === 'production') return null;
141
+ if (isProductionBoot(boot.boot.value)) return null;
143
142
  return adapter.devHint;
144
143
  });
145
144
 
@@ -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,10 +2,11 @@
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
12
  import {
@@ -21,11 +22,11 @@ import {
21
22
  } from 'vue';
22
23
 
23
24
  import {
24
- DEFAULT_SA_LOCALE,
25
- SA_INTL_LOCALES,
26
- isSaLocale,
27
- resolveMessages,
25
+ createSaCatalog,
26
+ type SaCatalog,
28
27
  type SaLocale,
28
+ type SaLocaleDefinition,
29
+ type SaLocaleOption,
29
30
  type SaMessages,
30
31
  type SaMessagesOverrides,
31
32
  } from '../client/i18n/index.js';
@@ -40,20 +41,35 @@ export interface SuperAdminI18n {
40
41
  intlLocale: ComputedRef<string>;
41
42
  /**
42
43
  * 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.
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.
45
46
  */
46
47
  switcherEnabled: boolean;
48
+ /** Locales the switcher offers, in order, with their display names. */
49
+ availableLocales: readonly SaLocaleOption[];
47
50
  }
48
51
 
49
52
  export interface SuperAdminI18nOptions {
50
53
  /**
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).
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).
54
58
  */
55
59
  locale?: SaLocale | Ref<SaLocale>;
56
- /** 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. */
57
73
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
58
74
  /**
59
75
  * Remembers the locale picked in the header switcher across reloads,
@@ -62,10 +78,15 @@ export interface SuperAdminI18nOptions {
62
78
  */
63
79
  persist?: boolean;
64
80
  /**
65
- * Storage adapter for `persist`. Defaults to `defaultKvStore()`. Pass a
66
- * key-prefixing wrapper when several apps share one origin.
81
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`.
67
82
  */
68
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;
69
90
  /**
70
91
  * Renders the language switcher in the shell chrome, default `true`. Set
71
92
  * `false` for a deployment that ships one language. A readonly `locale`
@@ -84,11 +105,16 @@ export const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n> = Symbol.for(
84
105
  );
85
106
 
86
107
  export function createSuperAdminI18n(options: SuperAdminI18nOptions = {}): SuperAdminI18n {
87
- const locale = isRef(options.locale) ? options.locale : createOwnedLocale(options);
88
- const messages = computed(() =>
89
- 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,
90
115
  );
91
- const intlLocale = computed(() => SA_INTL_LOCALES[locale.value]);
116
+ const messages = computed(() => catalog.messagesFor(active.value));
117
+ const intlLocale = computed(() => catalog.intlLocaleFor(active.value));
92
118
  // A readonly ref (typically a `computed` derived from a profile setting)
93
119
  // silently swallows writes, so the switcher would be a dead control. The
94
120
  // type system permits it — TypeScript ignores `readonly` when checking
@@ -98,7 +124,8 @@ export function createSuperAdminI18n(options: SuperAdminI18nOptions = {}): Super
98
124
  locale,
99
125
  messages,
100
126
  intlLocale,
101
- switcherEnabled: (options.switcher ?? true) && writable,
127
+ switcherEnabled: (options.switcher ?? true) && writable && catalog.locales.length > 1,
128
+ availableLocales: catalog.locales,
102
129
  };
103
130
  }
104
131
 
@@ -107,14 +134,18 @@ export function createSuperAdminI18n(options: SuperAdminI18nOptions = {}): Super
107
134
  * `locale` option, because that option is the app's default while the stored
108
135
  * value is the user's explicit pick in the switcher.
109
136
  */
110
- function createOwnedLocale(options: SuperAdminI18nOptions): Ref<SaLocale> {
111
- if (options.persist === false) {
112
- return ref(options.locale ?? DEFAULT_SA_LOCALE);
113
- }
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
+
114
141
  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));
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));
118
149
  return locale;
119
150
  }
120
151