@saasicat/ui-vue 0.16.0 → 0.18.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.
- package/README.md +12 -11
- package/dist/{messages-DetB75lX.d.ts → catalog-JXdKdIqG.d.cts} +104 -40
- package/dist/{messages-DetB75lX.d.cts → catalog-JXdKdIqG.d.ts} +104 -40
- package/dist/{chunk-TZ2NGQXK.js → chunk-CC4WK3DC.js} +40 -8
- package/dist/{chunk-CRUUZMMN.js → chunk-VRCVJTY4.js} +30 -4
- package/dist/{chunk-MGKJCVDU.js → chunk-Y5MWMWMD.js} +18 -18
- package/dist/client/index.cjs +70 -10
- package/dist/client/index.d.cts +51 -5
- package/dist/client/index.d.ts +51 -5
- package/dist/client/index.js +10 -4
- package/dist/index.cjs +85 -22
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +11 -5
- package/dist/quasar/index.cjs +54 -20
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +2 -2
- package/dist/{use-super-admin-i18n-DokHhkS_.d.cts → use-super-admin-i18n-1nCOkvgi.d.cts} +29 -9
- package/dist/{use-super-admin-i18n-BPC-H9jz.d.ts → use-super-admin-i18n-BlqcD0N7.d.ts} +29 -9
- package/package.json +8 -3
- package/src/client/i18n/catalog.ts +120 -0
- package/src/client/i18n/currency.ts +7 -2
- package/src/client/i18n/define.ts +2 -2
- package/src/client/i18n/index.ts +1 -0
- package/src/client/i18n/locale.ts +23 -11
- package/src/client/i18n/messages/bundles.ts +2 -3
- package/src/client/i18n/messages/marketing.ts +2 -2
- package/src/client/i18n/messages/nav.ts +3 -3
- package/src/client/i18n/messages.ts +7 -4
- package/src/client/index.ts +1 -0
- package/src/client/login-branding.ts +61 -0
- package/src/client/nav-builder.ts +33 -6
- package/src/components/LocaleSwitcher.vue +21 -12
- package/src/components/bundle-editor/BundleVersionStrip.vue +1 -1
- package/src/components/bundle-editor/bundle-version-status.ts +18 -11
- package/src/pages-standard/AdminLayout.vue +7 -3
- package/src/pages-standard/DiscoveryPage.vue +1 -1
- package/src/pages-standard/SuperAdminLoginPage.vue +8 -9
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +2 -3
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +4 -4
- package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +7 -7
- package/src/pages-standard/discovery-page/discovery-ui.ts +24 -15
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +2 -3
- package/src/pages-standard/plan-versions/format.ts +7 -8
- package/src/vue/use-super-admin-i18n.ts +59 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/ui-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.
|
|
56
|
+
"@saasicat/types": "^0.18.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.
|
|
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 {
|
|
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,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// must mirror that structure exactly, enforced at compile time via
|
|
4
4
|
// `TranslationOf`.
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { SaBuiltinLocale } from './locale.js';
|
|
7
7
|
|
|
8
8
|
/** Nested string map — the shape of every message namespace. */
|
|
9
9
|
export interface MessageTree {
|
|
@@ -27,7 +27,7 @@ export type PartialMessages<T> = {
|
|
|
27
27
|
export function defineMessages<T extends MessageTree>(
|
|
28
28
|
de: T,
|
|
29
29
|
en: TranslationOf<T>,
|
|
30
|
-
): Record<
|
|
30
|
+
): Record<SaBuiltinLocale, T> {
|
|
31
31
|
// `TranslationOf<T>` and `T` are structurally identical for string-valued
|
|
32
32
|
// trees; the cast only erases the mapped-type wrapper.
|
|
33
33
|
return { de, en: en as T };
|
package/src/client/i18n/index.ts
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Display locales of the SuperAdmin UI.
|
|
2
|
+
//
|
|
3
|
+
// The platform ships two complete catalogs — German (the reference that fixes
|
|
4
|
+
// the key structure) and English — and nothing beyond that. Which of them an
|
|
5
|
+
// app offers, whether it adds languages of its own, and what vocabulary it
|
|
6
|
+
// prefers is the app's call, made through `createSuperAdminApp({ i18n })`.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
/** Locales the platform itself ships a catalog for. */
|
|
9
|
+
export type SaBuiltinLocale = 'de' | 'en';
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Any locale the shell can run in: the built-ins plus whatever an app adds via
|
|
13
|
+
* `i18n.additionalLocales`. The `string & {}` arm keeps autocompletion for the
|
|
14
|
+
* built-ins while leaving the set open.
|
|
15
|
+
*/
|
|
16
|
+
export type SaLocale = SaBuiltinLocale | (string & {});
|
|
17
|
+
|
|
18
|
+
/** The locales the platform ships. Apps narrow or extend this, see `i18n`. */
|
|
19
|
+
export const SA_LOCALES: readonly SaBuiltinLocale[] = ['de', 'en'];
|
|
8
20
|
|
|
9
|
-
export const DEFAULT_SA_LOCALE:
|
|
21
|
+
export const DEFAULT_SA_LOCALE: SaBuiltinLocale = 'de';
|
|
10
22
|
|
|
11
|
-
/** BCP-47 tags used for `Intl`/`toLocaleString` formatting per
|
|
12
|
-
export const SA_INTL_LOCALES: Record<
|
|
23
|
+
/** BCP-47 tags used for `Intl`/`toLocaleString` formatting per built-in locale. */
|
|
24
|
+
export const SA_INTL_LOCALES: Record<SaBuiltinLocale, string> = {
|
|
13
25
|
de: 'de-DE',
|
|
14
26
|
en: 'en-US',
|
|
15
27
|
};
|
|
@@ -19,12 +31,12 @@ export const SA_INTL_LOCALES: Record<SaLocale, string> = {
|
|
|
19
31
|
* language, so the entries stay readable no matter which locale is active and
|
|
20
32
|
* never need translating.
|
|
21
33
|
*/
|
|
22
|
-
export const SA_LOCALE_LABELS: Record<
|
|
34
|
+
export const SA_LOCALE_LABELS: Record<SaBuiltinLocale, string> = {
|
|
23
35
|
de: 'Deutsch',
|
|
24
36
|
en: 'English',
|
|
25
37
|
};
|
|
26
38
|
|
|
27
|
-
/** Narrows untrusted input
|
|
28
|
-
export function
|
|
39
|
+
/** Narrows untrusted input to a locale the platform ships a catalog for. */
|
|
40
|
+
export function isSaBuiltinLocale(value: unknown): value is SaBuiltinLocale {
|
|
29
41
|
return typeof value === 'string' && (SA_LOCALES as readonly string[]).includes(value);
|
|
30
42
|
}
|
|
@@ -113,8 +113,7 @@ export const bundlesMessages = defineMessages(
|
|
|
113
113
|
bundleKeyHint: 'API-stabil · wird aus dem Label erzeugt',
|
|
114
114
|
errorKeyFormat: 'Nur A-Z, 0-9, Underscore; muss mit Buchstabe beginnen.',
|
|
115
115
|
errorKeyExists: 'Dieser Bundle-Key existiert bereits.',
|
|
116
|
-
descriptionPlaceholder:
|
|
117
|
-
'z. B. Kampagnen, WhatsApp und Korrespondenz für aktive Vereine.',
|
|
116
|
+
descriptionPlaceholder: 'z. B. Kampagnen, WhatsApp und Korrespondenz.',
|
|
118
117
|
sectionPricing: 'v1 · Pricing & Gültigkeit',
|
|
119
118
|
pricingHint: 'Monats- & Jahrespreis sowie Datum, ab dem das Bundle verkaufbar wird.',
|
|
120
119
|
validFromImmediate: '✓ Bundle ist nach Anlage sofort live und verkaufbar.',
|
|
@@ -341,7 +340,7 @@ export const bundlesMessages = defineMessages(
|
|
|
341
340
|
bundleKeyHint: 'API-stable · derived from the label',
|
|
342
341
|
errorKeyFormat: 'Only A-Z, 0-9, underscore; must start with a letter.',
|
|
343
342
|
errorKeyExists: 'This bundle key already exists.',
|
|
344
|
-
descriptionPlaceholder: 'e.g. Campaigns, WhatsApp and correspondence
|
|
343
|
+
descriptionPlaceholder: 'e.g. Campaigns, WhatsApp and correspondence.',
|
|
345
344
|
sectionPricing: 'v1 · pricing & validity',
|
|
346
345
|
pricingHint:
|
|
347
346
|
'Monthly & yearly price plus the date from which the bundle becomes sellable.',
|
|
@@ -76,7 +76,7 @@ export const marketingMessages = defineMessages(
|
|
|
76
76
|
ctaOverrideLabel: 'CTA-Text überschreiben (optional)',
|
|
77
77
|
ctaOverrideHint: 'leer lassen für Auto-Text',
|
|
78
78
|
topFeatureCount: '{count} Einträge',
|
|
79
|
-
featureLabelPlaceholder: 'z. B.
|
|
79
|
+
featureLabelPlaceholder: 'z. B. Erweiterte Berichte',
|
|
80
80
|
featureStrongPlaceholder: 'Highlight, z. B. bis 100',
|
|
81
81
|
moveUp: 'Nach oben',
|
|
82
82
|
moveDown: 'Nach unten',
|
|
@@ -209,7 +209,7 @@ export const marketingMessages = defineMessages(
|
|
|
209
209
|
ctaOverrideLabel: 'Override CTA text (optional)',
|
|
210
210
|
ctaOverrideHint: 'leave empty for the auto text',
|
|
211
211
|
topFeatureCount: '{count} entries',
|
|
212
|
-
featureLabelPlaceholder: 'e.g.
|
|
212
|
+
featureLabelPlaceholder: 'e.g. Advanced reporting',
|
|
213
213
|
featureStrongPlaceholder: 'Highlight, e.g. up to 100',
|
|
214
214
|
moveUp: 'Move up',
|
|
215
215
|
moveDown: 'Move down',
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
import type { StandardPageKey } from '@saasicat/types';
|
|
7
7
|
|
|
8
8
|
import { defineMessages } from '../define.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { SaBuiltinLocale } from '../locale.js';
|
|
10
10
|
|
|
11
11
|
// Type alias (not interface) so the shape keeps an implicit index signature
|
|
12
12
|
// and stays assignable to `MessageTree`.
|
|
13
|
-
type
|
|
13
|
+
export type SaNavMessages = {
|
|
14
14
|
readonly pages: Record<StandardPageKey, string>;
|
|
15
15
|
readonly sections: {
|
|
16
16
|
readonly overview: string;
|
|
@@ -20,7 +20,7 @@ type NavNamespace = {
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export const navMessages: Record<
|
|
23
|
+
export const navMessages: Record<SaBuiltinLocale, SaNavMessages> = defineMessages(
|
|
24
24
|
{
|
|
25
25
|
pages: {
|
|
26
26
|
dashboard: 'Dashboard',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// namespace files via `defineMessages`.
|
|
4
4
|
|
|
5
5
|
import { mergeMessages, type PartialMessages } from './define.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { SaBuiltinLocale } from './locale.js';
|
|
7
7
|
import { auditMessages } from './messages/audit.js';
|
|
8
8
|
import { bundlesMessages } from './messages/bundles.js';
|
|
9
9
|
import { commonMessages } from './messages/common.js';
|
|
@@ -46,7 +46,7 @@ export type SaMessages = {
|
|
|
46
46
|
/** Shape of app-side string overrides (deep partial of the catalog). */
|
|
47
47
|
export type SaMessagesOverrides = PartialMessages<SaMessages>;
|
|
48
48
|
|
|
49
|
-
function catalogFor(locale:
|
|
49
|
+
function catalogFor(locale: SaBuiltinLocale): SaMessages {
|
|
50
50
|
return {
|
|
51
51
|
common: commonMessages[locale],
|
|
52
52
|
nav: navMessages[locale],
|
|
@@ -69,7 +69,7 @@ function catalogFor(locale: SaLocale): SaMessages {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Complete, immutable message catalogs per locale. */
|
|
72
|
-
export const SA_MESSAGES: Record<
|
|
72
|
+
export const SA_MESSAGES: Record<SaBuiltinLocale, SaMessages> = {
|
|
73
73
|
de: catalogFor('de'),
|
|
74
74
|
en: catalogFor('en'),
|
|
75
75
|
};
|
|
@@ -78,7 +78,10 @@ export const SA_MESSAGES: Record<SaLocale, SaMessages> = {
|
|
|
78
78
|
* Returns the platform catalog for `locale`, optionally overlaid with
|
|
79
79
|
* app-side overrides.
|
|
80
80
|
*/
|
|
81
|
-
export function resolveMessages(
|
|
81
|
+
export function resolveMessages(
|
|
82
|
+
locale: SaBuiltinLocale,
|
|
83
|
+
overrides?: SaMessagesOverrides,
|
|
84
|
+
): SaMessages {
|
|
82
85
|
if (!overrides) return SA_MESSAGES[locale];
|
|
83
86
|
return mergeMessages(SA_MESSAGES[locale], overrides);
|
|
84
87
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Brand presentation of the login and first-run cards: what the public boot
|
|
2
|
+
// endpoint reports, falling back to the branding the app configured.
|
|
3
|
+
//
|
|
4
|
+
// Pure projections, kept out of the SFC so they can be tested without a DOM —
|
|
5
|
+
// and so the guards below cannot quietly go missing again. A boot response
|
|
6
|
+
// whose `project` is absent used to throw here and take the whole card with it
|
|
7
|
+
// on the next render.
|
|
8
|
+
|
|
9
|
+
/** The slice of the public boot response these cards read. */
|
|
10
|
+
export interface LoginBootProject {
|
|
11
|
+
displayName?: string | null;
|
|
12
|
+
label?: string | null;
|
|
13
|
+
icon?: string | null;
|
|
14
|
+
logoUrl?: string | null;
|
|
15
|
+
environment?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** App-configured branding, used wherever boot says nothing. */
|
|
19
|
+
export interface LoginBrandFallback {
|
|
20
|
+
name?: string;
|
|
21
|
+
tag?: string;
|
|
22
|
+
logoText?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LoginBranding {
|
|
26
|
+
name: string;
|
|
27
|
+
tag: string;
|
|
28
|
+
icon: string;
|
|
29
|
+
logoUrl: string | null;
|
|
30
|
+
/** Environment worth showing, i.e. anything but production. `null` hides it. */
|
|
31
|
+
environment: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_TAG = 'SuperAdmin';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `boot` is whatever the endpoint returned — possibly `null`, possibly an
|
|
38
|
+
* object without `project`. Both are treated as "boot said nothing" rather
|
|
39
|
+
* than as a reason to fail.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveLoginBranding(
|
|
42
|
+
boot: { project?: LoginBootProject | null } | null | undefined,
|
|
43
|
+
fallback: LoginBrandFallback = {},
|
|
44
|
+
): LoginBranding {
|
|
45
|
+
const project = boot?.project ?? null;
|
|
46
|
+
const environment = project?.environment ?? null;
|
|
47
|
+
return {
|
|
48
|
+
name: project?.displayName || fallback.name || '',
|
|
49
|
+
tag: project?.label || fallback.tag || DEFAULT_TAG,
|
|
50
|
+
icon: project?.icon || fallback.logoText || '',
|
|
51
|
+
logoUrl: project?.logoUrl || null,
|
|
52
|
+
environment: environment && environment !== 'production' ? environment : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Test credentials must never be printed in production, whatever boot omits. */
|
|
57
|
+
export function isProductionBoot(
|
|
58
|
+
boot: { project?: LoginBootProject | null } | null | undefined,
|
|
59
|
+
): boolean {
|
|
60
|
+
return boot?.project?.environment === 'production';
|
|
61
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import type { AdminManifest, StandardPageDef, StandardPageKey } from '@saasicat/types';
|
|
13
13
|
|
|
14
|
-
import { DEFAULT_SA_LOCALE, type SaLocale } from './i18n/locale.js';
|
|
15
|
-
import { navMessages } from './i18n/messages/nav.js';
|
|
14
|
+
import { DEFAULT_SA_LOCALE, isSaBuiltinLocale, type SaLocale } from './i18n/locale.js';
|
|
15
|
+
import { navMessages, type SaNavMessages } from './i18n/messages/nav.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Default routes for the platform standard pages. Consumers may override this
|
|
@@ -64,8 +64,17 @@ export interface NavBuilderOptions {
|
|
|
64
64
|
* UI locale for the default labels and section names, default `'de'`.
|
|
65
65
|
* Pass the same locale to `buildSidebar()`'s `sectionOrder` (via
|
|
66
66
|
* `defaultSectionOrder(locale)`) — section names are compared as strings.
|
|
67
|
+
*
|
|
68
|
+
* Only resolves the two catalogs the platform ships. Prefer `nav`, which
|
|
69
|
+
* carries app overrides and app-supplied languages as well.
|
|
67
70
|
*/
|
|
68
71
|
locale?: SaLocale;
|
|
72
|
+
/**
|
|
73
|
+
* Resolved `nav` namespace, as `useSuperAdminI18n().messages.value.nav`
|
|
74
|
+
* hands it out. Wins over `locale`, and is the only way the sidebar sees
|
|
75
|
+
* `i18n.overrides` or a language the app added itself.
|
|
76
|
+
*/
|
|
77
|
+
nav?: SaNavMessages;
|
|
69
78
|
/**
|
|
70
79
|
* Optional: overrides the default routes for certain standard pages.
|
|
71
80
|
* Consumers set this if they want an alternative URL structure.
|
|
@@ -123,13 +132,31 @@ const PAGE_SECTIONS: Partial<Record<StandardPageKey, SectionKey>> = {
|
|
|
123
132
|
platformEmailHistory: 'system',
|
|
124
133
|
};
|
|
125
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The catalog to draw labels from. `nav` wins because it already carries app
|
|
137
|
+
* overrides and app-supplied languages; `locale` only ever reaches the two
|
|
138
|
+
* catalogs the platform ships.
|
|
139
|
+
*/
|
|
140
|
+
function resolveNav(options: NavBuilderOptions): SaNavMessages {
|
|
141
|
+
return options.nav ?? builtinNav(options.locale);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** An unknown locale falls back rather than crashing the whole sidebar. */
|
|
145
|
+
function builtinNav(locale: SaLocale | undefined): SaNavMessages {
|
|
146
|
+
return navMessages[isSaBuiltinLocale(locale) ? locale : DEFAULT_SA_LOCALE];
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
/**
|
|
127
150
|
* Localized default section names in drawer order (Übersicht → Produktkatalog
|
|
128
151
|
* → Kunden → System). Pass the result to `buildSidebar()` when building routes
|
|
129
|
-
* with a non-default locale
|
|
152
|
+
* with a non-default locale, or hand it the resolved `nav` catalog directly —
|
|
153
|
+
* section names are compared as strings, so both sides must come from the same
|
|
154
|
+
* source.
|
|
130
155
|
*/
|
|
131
|
-
export function defaultSectionOrder(
|
|
132
|
-
|
|
156
|
+
export function defaultSectionOrder(
|
|
157
|
+
locale: SaLocale | SaNavMessages = DEFAULT_SA_LOCALE,
|
|
158
|
+
): readonly string[] {
|
|
159
|
+
const sections = typeof locale === 'string' ? builtinNav(locale).sections : locale.sections;
|
|
133
160
|
return [sections.overview, sections.catalog, sections.customers, sections.system];
|
|
134
161
|
}
|
|
135
162
|
|
|
@@ -149,7 +176,7 @@ export function buildRoutes(
|
|
|
149
176
|
|
|
150
177
|
// StandardPages
|
|
151
178
|
const standard = manifest.navigation?.standardPages ?? {};
|
|
152
|
-
const nav =
|
|
179
|
+
const nav = resolveNav(options);
|
|
153
180
|
const defaultNavSections: Partial<Record<StandardPageKey, string>> = Object.fromEntries(
|
|
154
181
|
Object.entries(PAGE_SECTIONS).map(([page, section]) => [page, nav.sections[section]]),
|
|
155
182
|
);
|
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
dense
|
|
7
7
|
no-caps
|
|
8
8
|
icon="language"
|
|
9
|
-
:label="
|
|
9
|
+
:label="activeLabel"
|
|
10
10
|
:aria-label="msg.header.language"
|
|
11
11
|
>
|
|
12
12
|
<q-menu auto-close anchor="bottom right" self="top right">
|
|
13
13
|
<q-list dense class="sa-locale-switcher__list">
|
|
14
14
|
<q-item
|
|
15
|
-
v-for="option in
|
|
16
|
-
:key="option"
|
|
15
|
+
v-for="option in availableLocales"
|
|
16
|
+
:key="option.code"
|
|
17
17
|
clickable
|
|
18
|
-
:active="option === locale"
|
|
19
|
-
@click="select(option)"
|
|
18
|
+
:active="option.code === locale"
|
|
19
|
+
@click="select(option.code)"
|
|
20
20
|
>
|
|
21
|
-
<q-item-section>{{
|
|
22
|
-
<q-item-section v-if="option === locale" side>
|
|
21
|
+
<q-item-section>{{ option.label }}</q-item-section>
|
|
22
|
+
<q-item-section v-if="option.code === locale" side>
|
|
23
23
|
<q-icon name="check" size="16px" />
|
|
24
24
|
</q-item-section>
|
|
25
25
|
</q-item>
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
</template>
|
|
30
30
|
|
|
31
31
|
<script setup lang="ts">
|
|
32
|
-
import {
|
|
32
|
+
import { computed } from 'vue';
|
|
33
|
+
|
|
34
|
+
import type { SaLocale, SaLocaleOption } from '../client/i18n/index.js';
|
|
33
35
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
34
36
|
|
|
35
37
|
// Language switcher for the shell chrome. Writes straight to the shared i18n
|
|
@@ -39,11 +41,11 @@ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js
|
|
|
39
41
|
// The button inherits its color from the surrounding chrome (`currentColor`),
|
|
40
42
|
// so it needs no variant prop — each host sets the color it wants.
|
|
41
43
|
//
|
|
42
|
-
// It renders nothing when the app opted out
|
|
43
|
-
// locale it does not allow writing to; hosts therefore embed it
|
|
44
|
-
// and never have to repeat that check.
|
|
44
|
+
// It renders nothing when the app opted out, offers a single language, or
|
|
45
|
+
// handed over a locale it does not allow writing to; hosts therefore embed it
|
|
46
|
+
// unconditionally and never have to repeat that check.
|
|
45
47
|
|
|
46
|
-
const { locale, switcherEnabled } = useSuperAdminI18n();
|
|
48
|
+
const { locale, switcherEnabled, availableLocales } = useSuperAdminI18n();
|
|
47
49
|
|
|
48
50
|
// Only an explicit `false` hides the switcher. The injection key is a
|
|
49
51
|
// `Symbol.for`, so a context created by an older copy of this package resolves
|
|
@@ -53,6 +55,13 @@ const { locale, switcherEnabled } = useSuperAdminI18n();
|
|
|
53
55
|
const visible = switcherEnabled !== false;
|
|
54
56
|
const msg = useSaMessages('shell');
|
|
55
57
|
|
|
58
|
+
// Same reason as above: such a context carries no `availableLocales` either, so
|
|
59
|
+
// the list has to tolerate its absence instead of throwing during render.
|
|
60
|
+
const options = computed<readonly SaLocaleOption[]>(() => availableLocales ?? []);
|
|
61
|
+
const activeLabel = computed(
|
|
62
|
+
() => options.value.find((option) => option.code === locale.value)?.label ?? locale.value,
|
|
63
|
+
);
|
|
64
|
+
|
|
56
65
|
function select(next: SaLocale): void {
|
|
57
66
|
locale.value = next;
|
|
58
67
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
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';
|
|
10
|
-
import {
|
|
9
|
+
import { SA_INTL_LOCALES, type SaBuiltinLocale, type SaLocale } from '../../client/i18n/locale.js';
|
|
10
|
+
import type { SaMessages } from '../../client/i18n/messages.js';
|
|
11
11
|
|
|
12
12
|
/** UI lifecycle status of a single BundleVersion. */
|
|
13
13
|
export type BundleVersionUiStatus = 'draft' | 'live' | 'scheduled' | 'superseded';
|
|
@@ -62,12 +62,16 @@ const BUNDLE_STATUS_CLASS: Record<BundleAggregateStatus, BundleStatusMeta['cls']
|
|
|
62
62
|
retired: 'supersed',
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Label, chip class and tooltip for a status. Takes the resolved slice rather
|
|
67
|
+
* than a locale code — that is what lets an app-supplied language and
|
|
68
|
+
* `i18n.overrides` reach these labels.
|
|
69
|
+
*/
|
|
66
70
|
export function bundleStatusMeta(
|
|
67
71
|
status: BundleAggregateStatus,
|
|
68
|
-
|
|
72
|
+
bundles: SaMessages['bundles'],
|
|
69
73
|
): BundleStatusMeta {
|
|
70
|
-
const texts =
|
|
74
|
+
const texts = bundles.status[status];
|
|
71
75
|
return { label: texts.label, cls: BUNDLE_STATUS_CLASS[status], tooltip: texts.tooltip };
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -155,10 +159,13 @@ export function formatDate(iso: string | null | undefined, locale: SaLocale): st
|
|
|
155
159
|
const day = iso.slice(0, 10);
|
|
156
160
|
const [y, m, d] = day.split('-');
|
|
157
161
|
if (!y || !m || !d) return iso;
|
|
158
|
-
return new Date(`${day}T00:00:00Z`).toLocaleDateString(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
return new Date(`${day}T00:00:00Z`).toLocaleDateString(
|
|
163
|
+
SA_INTL_LOCALES[locale as SaBuiltinLocale] ?? locale,
|
|
164
|
+
{
|
|
165
|
+
day: '2-digit',
|
|
166
|
+
month: '2-digit',
|
|
167
|
+
year: 'numeric',
|
|
168
|
+
timeZone: 'UTC',
|
|
169
|
+
},
|
|
170
|
+
);
|
|
164
171
|
}
|