@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
|
@@ -189,7 +189,7 @@ const emit = defineEmits<{
|
|
|
189
189
|
|
|
190
190
|
const route = useRoute();
|
|
191
191
|
const msg = useSaMessages('shell');
|
|
192
|
-
const {
|
|
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
|
-
|
|
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(
|
|
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) => ({
|
|
@@ -195,7 +195,7 @@ const statusFilterOptions = computed<Array<{ label: string; value: DiscoveryStat
|
|
|
195
195
|
() => [
|
|
196
196
|
{ label: msg.value.statusFilterAll, value: 'all' },
|
|
197
197
|
...(['pending', 'approved', 'outdated', 'obsolete'] as const).map((status) => ({
|
|
198
|
-
label: statusLabel(status,
|
|
198
|
+
label: statusLabel(status, msg.value),
|
|
199
199
|
value: status,
|
|
200
200
|
})),
|
|
201
201
|
],
|
|
@@ -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
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
|
141
|
+
if (isProductionBoot(boot.boot.value)) return null;
|
|
143
142
|
return adapter.devHint;
|
|
144
143
|
});
|
|
145
144
|
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
type BundleAggregateStatus,
|
|
51
51
|
} from '../../components/bundle-editor/bundle-version-status.js';
|
|
52
52
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
53
|
-
import { useSaMessages
|
|
53
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
54
54
|
|
|
55
55
|
const props = defineProps<{
|
|
56
56
|
filteredBundles: BundleRow[];
|
|
@@ -69,10 +69,9 @@ const emit = defineEmits<{
|
|
|
69
69
|
}>();
|
|
70
70
|
|
|
71
71
|
const msg = useSaMessages('bundles');
|
|
72
|
-
const { locale } = useSuperAdminI18n();
|
|
73
72
|
|
|
74
73
|
function statusMetaOf(bundle: BundleRow) {
|
|
75
|
-
return bundleStatusMeta(props.aggregateStatusOf(bundle),
|
|
74
|
+
return bundleStatusMeta(props.aggregateStatusOf(bundle), msg.value);
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
function statusClass(bundle: BundleRow): string {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
<div class="sa-trans-fields">
|
|
11
11
|
<div v-for="f in fields" :key="f" class="sa-trans-field">
|
|
12
|
-
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f,
|
|
12
|
+
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f, msg, common) }}</label>
|
|
13
13
|
<q-input
|
|
14
14
|
v-if="f !== 'unit'"
|
|
15
15
|
dense
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
<div class="sa-trans-fields">
|
|
49
49
|
<div v-for="f in fields" :key="f" class="sa-trans-field">
|
|
50
|
-
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f,
|
|
50
|
+
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f, msg, common) }}</label>
|
|
51
51
|
<q-input
|
|
52
52
|
dense
|
|
53
53
|
outlined
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
import { computed, reactive } from 'vue';
|
|
72
72
|
import type { CatalogEntryI18nFields } from '@saasicat/types';
|
|
73
73
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
74
|
-
import { useSaMessages
|
|
74
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
75
75
|
import {
|
|
76
76
|
DISCOVERY_DEFAULT_LOCALE,
|
|
77
77
|
i18nFieldLabel,
|
|
@@ -107,7 +107,7 @@ const emit = defineEmits<{
|
|
|
107
107
|
|
|
108
108
|
const msg = useSaMessages('discovery');
|
|
109
109
|
// Renamed: `locale` is the catalog-entry data locale everywhere else in this file.
|
|
110
|
-
const
|
|
110
|
+
const common = useSaMessages('common');
|
|
111
111
|
|
|
112
112
|
const targetLocales = computed(() => props.activeLocales.filter((l) => l !== props.defaultLocale));
|
|
113
113
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="sa-stc" @click.stop>
|
|
3
3
|
<span class="sa-review" :class="`sa-review--${status}`">
|
|
4
|
-
{{ statusLabel(status,
|
|
5
|
-
<q-tooltip>{{ statusHint(status,
|
|
4
|
+
{{ statusLabel(status, msg) }}
|
|
5
|
+
<q-tooltip>{{ statusHint(status, msg) }}</q-tooltip>
|
|
6
6
|
</span>
|
|
7
7
|
<q-btn
|
|
8
8
|
dense
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
:loading="busy"
|
|
16
16
|
@click="emit('set-status', primary.to)"
|
|
17
17
|
>
|
|
18
|
-
<q-tooltip>{{ statusHint(primary.to,
|
|
18
|
+
<q-tooltip>{{ statusHint(primary.to, msg) }}</q-tooltip>
|
|
19
19
|
</q-btn>
|
|
20
20
|
<q-btn v-if="menu.length" dense flat round size="sm" icon="more_horiz">
|
|
21
21
|
<q-menu auto-close anchor="bottom right" self="top right">
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
40
|
import { computed } from 'vue';
|
|
41
41
|
import type { DiscoveryStatus } from '@saasicat/types';
|
|
42
|
-
import {
|
|
42
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
43
43
|
import { primaryReviewAction, reviewMenuActions, statusHint, statusLabel } from './discovery-ui.js';
|
|
44
44
|
|
|
45
45
|
// StatusControl (#20, design sim): status chip + contextual primary action
|
|
@@ -57,10 +57,10 @@ const emit = defineEmits<{
|
|
|
57
57
|
'set-status': [target: DiscoveryStatus];
|
|
58
58
|
}>();
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const msg = useSaMessages('discovery');
|
|
61
61
|
|
|
62
|
-
const primary = computed(() => primaryReviewAction(props.status,
|
|
63
|
-
const menu = computed(() => reviewMenuActions(props.status,
|
|
62
|
+
const primary = computed(() => primaryReviewAction(props.status, msg.value));
|
|
63
|
+
const menu = computed(() => reviewMenuActions(props.status, msg.value));
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
66
|
<style scoped>
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// labels/colors/coverage calculations.
|
|
5
5
|
|
|
6
6
|
import type { CatalogEntryI18n, DiscoveryStatus } from '@saasicat/types';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import type { SaMessages } from '../../client/i18n/messages.js';
|
|
8
|
+
|
|
9
|
+
// The resolved slices these helpers render from. Taking the catalog rather
|
|
10
|
+
// than a locale code is what lets an app-supplied language and `i18n.overrides`
|
|
11
|
+
// reach them — a raw `discoveryMessages[locale]` lookup only ever sees the two
|
|
12
|
+
// catalogs the platform ships.
|
|
13
|
+
type SaDiscoveryMessages = SaMessages['discovery'];
|
|
14
|
+
type SaCommonMessages = SaMessages['common'];
|
|
10
15
|
|
|
11
16
|
/** Translatable fields of a catalog entry. */
|
|
12
17
|
export type I18nField = 'label' | 'description' | 'unit';
|
|
@@ -42,20 +47,24 @@ export function localeFull(locale: string): string {
|
|
|
42
47
|
return LOCALE_NAMES[locale]?.full ?? locale;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
export function i18nFieldLabel(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
export function i18nFieldLabel(
|
|
51
|
+
f: I18nField,
|
|
52
|
+
discovery: SaDiscoveryMessages,
|
|
53
|
+
common: SaCommonMessages,
|
|
54
|
+
): string {
|
|
55
|
+
if (f === 'label') return discovery.trans.fieldLabel;
|
|
56
|
+
if (f === 'description') return common.description;
|
|
57
|
+
return discovery.unit;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
// ─── Approval lifecycle (#20): status display + state machine ────────────────
|
|
52
61
|
|
|
53
|
-
export function statusLabel(status: DiscoveryStatus,
|
|
54
|
-
return
|
|
62
|
+
export function statusLabel(status: DiscoveryStatus, discovery: SaDiscoveryMessages): string {
|
|
63
|
+
return discovery.statusLabels[status];
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
export function statusHint(status: DiscoveryStatus,
|
|
58
|
-
return
|
|
66
|
+
export function statusHint(status: DiscoveryStatus, discovery: SaDiscoveryMessages): string {
|
|
67
|
+
return discovery.statusHints[status];
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export interface ReviewAction {
|
|
@@ -69,9 +78,9 @@ export interface ReviewAction {
|
|
|
69
78
|
/** Primary action per status (design sim `StatusControl`, #20). */
|
|
70
79
|
export function primaryReviewAction(
|
|
71
80
|
status: DiscoveryStatus,
|
|
72
|
-
|
|
81
|
+
discovery: SaDiscoveryMessages,
|
|
73
82
|
): ReviewAction {
|
|
74
|
-
const actions =
|
|
83
|
+
const actions = discovery.reviewActions;
|
|
75
84
|
switch (status) {
|
|
76
85
|
case 'pending':
|
|
77
86
|
return { label: actions.approve, to: 'approved', emphasized: true };
|
|
@@ -87,9 +96,9 @@ export function primaryReviewAction(
|
|
|
87
96
|
/** Kebab menu actions per status (design sim `StatusControl`, #20). */
|
|
88
97
|
export function reviewMenuActions(
|
|
89
98
|
status: DiscoveryStatus,
|
|
90
|
-
|
|
99
|
+
discovery: SaDiscoveryMessages,
|
|
91
100
|
): ReviewAction[] {
|
|
92
|
-
const actions =
|
|
101
|
+
const actions = discovery.reviewActions;
|
|
93
102
|
const markObsolete: ReviewAction = {
|
|
94
103
|
label: actions.markObsolete,
|
|
95
104
|
to: 'obsolete',
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<script setup lang="ts">
|
|
74
74
|
import { computed } from 'vue';
|
|
75
75
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
76
|
-
import { useSaMessages
|
|
76
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
77
77
|
import type { CatalogSnapshot } from './catalog-history.types.js';
|
|
78
78
|
import { formatRelative } from './format.js';
|
|
79
79
|
|
|
@@ -91,7 +91,6 @@ const emit = defineEmits<{
|
|
|
91
91
|
}>();
|
|
92
92
|
|
|
93
93
|
const msg = useSaMessages('planVersions');
|
|
94
|
-
const { locale } = useSuperAdminI18n();
|
|
95
94
|
|
|
96
95
|
const snapshotCountLabel = computed(() =>
|
|
97
96
|
formatMessage(msg.value.timeline.snapshotCount, { count: props.snapshots.length }),
|
|
@@ -109,7 +108,7 @@ function metaText(s: CatalogSnapshot): string {
|
|
|
109
108
|
? msg.value.timeline.noOpenDrafts
|
|
110
109
|
: formatMessage(msg.value.timeline.openCount, { count: s.draftCount });
|
|
111
110
|
}
|
|
112
|
-
if (s.publishedAt) return formatRelative(s.publishedAt,
|
|
111
|
+
if (s.publishedAt) return formatRelative(s.publishedAt, msg.value);
|
|
113
112
|
return msg.value.timeline.unknown;
|
|
114
113
|
}
|
|
115
114
|
</script>
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
// Formatting helper retained for the reusable catalog-history timeline.
|
|
2
2
|
|
|
3
3
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
4
|
-
import {
|
|
5
|
-
import { planVersionsMessages } from '../../client/i18n/messages/plan-versions.js';
|
|
4
|
+
import type { SaMessages } from '../../client/i18n/messages.js';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
// The resolved slice, not a locale code: only this way do an app-supplied
|
|
7
|
+
// language and `i18n.overrides` reach the relative-date wording.
|
|
8
|
+
type SaPlanVersionsMessages = SaMessages['planVersions'];
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
return planVersionsMessages[locale].format;
|
|
11
|
-
}
|
|
10
|
+
const EMPTY_VALUE = '—';
|
|
12
11
|
|
|
13
12
|
export function formatRelative(
|
|
14
13
|
iso: string | undefined,
|
|
15
|
-
|
|
14
|
+
planVersions: SaPlanVersionsMessages,
|
|
16
15
|
now: Date = new Date(),
|
|
17
16
|
): string {
|
|
18
17
|
if (!iso) return EMPTY_VALUE;
|
|
19
|
-
const msg =
|
|
18
|
+
const msg = planVersions.format;
|
|
20
19
|
const d = new Date(iso);
|
|
21
20
|
const diffMs = now.getTime() - d.getTime();
|
|
22
21
|
const days = Math.floor(diffMs / 86_400_000);
|
|
@@ -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
|
|
6
|
-
// (
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
/**
|
|
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()`.
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
|