@cosmicdrift/kumiko-bundled-features 0.130.0 → 0.130.2
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/package.json +6 -6
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +17 -1
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +11 -2
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +2 -0
- package/src/admin-shell/feature.ts +2 -1
- package/src/admin-shell/i18n.ts +1 -0
- package/src/admin-shell/overview-allowlist.ts +5 -1
- package/src/admin-shell/web/platform-overview-screen.tsx +18 -1
- package/src/audit/__tests__/audit-screens.boot.test.ts +15 -2
- package/src/audit/constants.ts +4 -0
- package/src/audit/feature.ts +10 -1
- package/src/audit/handlers/details.query.ts +47 -0
- package/src/audit/i18n.ts +6 -2
- package/src/audit/web/audit-log-detail-screen.tsx +133 -0
- package/src/audit/web/audit-log-screen.tsx +141 -124
- package/src/audit/web/client-plugin.tsx +3 -1
- package/src/audit/web/index.ts +1 -0
- package/src/jobs/feature.ts +1 -0
- package/src/jobs/i18n.ts +3 -2
- package/src/jobs/web/job-run-detail-screen.tsx +25 -12
- package/src/jobs/web/job-runs-screen.tsx +79 -63
- package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
- package/src/text-content/web/client-plugin.tsx +124 -104
- package/src/tier-engine/web/tier-admin-screen.tsx +2 -2
- package/src/user/constants.ts +1 -0
- package/src/user-data-rights/feature.ts +1 -6
- package/src/user-data-rights/i18n.ts +4 -0
- package/src/user-data-rights/web/i18n.ts +22 -1
|
@@ -53,7 +53,7 @@ describe("groupBlocksByFolder", () => {
|
|
|
53
53
|
args: { slug: "imprint", lang: "de" },
|
|
54
54
|
});
|
|
55
55
|
expect(root.children).toBeUndefined();
|
|
56
|
-
expect(root.icon).
|
|
56
|
+
expect(root.icon).toBe("file");
|
|
57
57
|
});
|
|
58
58
|
test('folder="page" → Folder-Container mit child', () => {
|
|
59
59
|
const nodes = inside(
|
|
@@ -101,7 +101,7 @@ describe("groupBlocksByFolder", () => {
|
|
|
101
101
|
);
|
|
102
102
|
expect(nodes).toHaveLength(2);
|
|
103
103
|
expect(nodes[0]?.label).toBe("imprint");
|
|
104
|
-
expect(nodes[0]?.icon).
|
|
104
|
+
expect(nodes[0]?.icon).toBe("file");
|
|
105
105
|
expect(nodes[1]?.label).toBe("page");
|
|
106
106
|
expect(nodes[1]?.icon).toBe("folder");
|
|
107
107
|
});
|
|
@@ -184,12 +184,37 @@ describe("groupBlocksByFolder", () => {
|
|
|
184
184
|
const leaf = nodes[0];
|
|
185
185
|
const folder = nodes[1];
|
|
186
186
|
expect(leaf?.label).toBe("Page-Root");
|
|
187
|
-
expect(leaf?.icon).
|
|
187
|
+
expect(leaf?.icon).toBe("file");
|
|
188
188
|
expect(leaf?.target).toBeDefined();
|
|
189
189
|
expect(folder?.label).toBe("page");
|
|
190
190
|
expect(folder?.icon).toBe("folder");
|
|
191
191
|
expect(folder?.target).toBeUndefined();
|
|
192
192
|
});
|
|
193
|
+
test("tenantIdOverride wird in die Edit-Target-Args gereicht (SystemAdmin SYSTEM-Tenant-Content)", () => {
|
|
194
|
+
const nodes = inside(
|
|
195
|
+
groupBlocksByFolder(
|
|
196
|
+
[block({ slug: "imprint", folder: null }), block({ slug: "hero", folder: "page" })],
|
|
197
|
+
"00000000-0000-4000-8000-000000000000",
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
// Root-Leaf trägt den Override.
|
|
201
|
+
expect(nodes[0]?.target?.args).toEqual({
|
|
202
|
+
slug: "imprint",
|
|
203
|
+
lang: "de",
|
|
204
|
+
tenantIdOverride: "00000000-0000-4000-8000-000000000000",
|
|
205
|
+
});
|
|
206
|
+
// Auch der Leaf im Folder (rekursiv durchgereicht).
|
|
207
|
+
const folderChild = childrenArray(nodes[1]?.children)[0];
|
|
208
|
+
expect(folderChild?.target?.args).toEqual({
|
|
209
|
+
slug: "hero",
|
|
210
|
+
lang: "de",
|
|
211
|
+
tenantIdOverride: "00000000-0000-4000-8000-000000000000",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
test("ohne tenantIdOverride bleiben die Args frei davon (Default: Session-Tenant)", () => {
|
|
215
|
+
const nodes = inside(groupBlocksByFolder([block({ slug: "imprint", folder: null })]));
|
|
216
|
+
expect(nodes[0]?.target?.args).toEqual({ slug: "imprint", lang: "de" });
|
|
217
|
+
});
|
|
193
218
|
test("Wrapper-Folder 'Content' umschließt alle blocks", () => {
|
|
194
219
|
// V.1.5d Wrapper-Convention: groupBlocksByFolder gibt EINEN Knoten
|
|
195
220
|
// zurück (den Wrapper), Inhalt liegt eine Ebene tiefer.
|
|
@@ -74,13 +74,21 @@ function newFolderNode(): FolderNode {
|
|
|
74
74
|
return { leaves: [], subFolders: new Map() };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function attachBlock(root: FolderNode, block: BlockSummary): void {
|
|
77
|
+
function attachBlock(root: FolderNode, block: BlockSummary, tenantIdOverride?: string): void {
|
|
78
78
|
const leaf: TreeNode = {
|
|
79
79
|
label: block.title || block.slug,
|
|
80
|
+
icon: "file",
|
|
80
81
|
target: {
|
|
81
82
|
featureId: "text-content",
|
|
82
83
|
action: "edit",
|
|
83
|
-
|
|
84
|
+
// tenantIdOverride travels with the node → the editor's by-slug read +
|
|
85
|
+
// set write target the same tenant the tree was loaded from (SystemAdmin
|
|
86
|
+
// editing SYSTEM-tenant content). Omitted → session tenant.
|
|
87
|
+
args: {
|
|
88
|
+
slug: block.slug,
|
|
89
|
+
lang: block.lang,
|
|
90
|
+
...(tenantIdOverride !== undefined && { tenantIdOverride }),
|
|
91
|
+
},
|
|
84
92
|
},
|
|
85
93
|
state: block.body ? "filled" : "stub",
|
|
86
94
|
};
|
|
@@ -116,10 +124,13 @@ function renderFolderNode(node: FolderNode): TreeNode[] {
|
|
|
116
124
|
return result;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
export function groupBlocksByFolder(
|
|
127
|
+
export function groupBlocksByFolder(
|
|
128
|
+
blocks: readonly BlockSummary[],
|
|
129
|
+
tenantIdOverride?: string,
|
|
130
|
+
): readonly TreeNode[] {
|
|
120
131
|
const root = newFolderNode();
|
|
121
132
|
for (const block of blocks) {
|
|
122
|
-
attachBlock(root, block);
|
|
133
|
+
attachBlock(root, block, tenantIdOverride);
|
|
123
134
|
}
|
|
124
135
|
const rendered = renderFolderNode(root);
|
|
125
136
|
if (rendered.length === 0) return [];
|
|
@@ -133,47 +144,52 @@ export function groupBlocksByFolder(blocks: readonly BlockSummary[]): readonly T
|
|
|
133
144
|
];
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const text = await r.text().catch(() => r.statusText);
|
|
155
|
-
throw new Error(`text-content load failed: ${r.status} ${text}`);
|
|
156
|
-
}
|
|
157
|
-
return r.json();
|
|
158
|
-
})
|
|
159
|
-
.then((data: ByTenantResponse) => {
|
|
160
|
-
// Der App-seitige r.nav-Knoten IST der "Content"-Container → die
|
|
161
|
-
// Provider-Kinder sind die Folder/Leaves darunter, nicht der Wrapper.
|
|
162
|
-
const content = groupBlocksByFolder(data.data.blocks)[0];
|
|
163
|
-
emit(content !== undefined && Array.isArray(content.children) ? content.children : []);
|
|
147
|
+
// tenantIdOverride (SystemAdmin-only) lets an app point the Content tree at a
|
|
148
|
+
// tenant other than the session's — publicstatus seeds marketing/legal blocks
|
|
149
|
+
// on SYSTEM_TENANT_ID, so its SystemAdmin must read that tenant, not their own.
|
|
150
|
+
function makeTreeProvider(tenantIdOverride?: string): TreeChildrenSubscribe {
|
|
151
|
+
return () => (emit, emitError) => {
|
|
152
|
+
// CSRF-Header bei authenticated requests pflicht (auth-middleware
|
|
153
|
+
// double-submit pattern). Anonymous/Pre-Login wäre csrf-token=undefined
|
|
154
|
+
// → header weggelassen → server lässt die anonymous-Variante durch.
|
|
155
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
156
|
+
const csrf = readCsrfToken();
|
|
157
|
+
if (csrf !== undefined) headers[CSRF_HEADER_NAME] = csrf;
|
|
158
|
+
fetch("/api/query", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers,
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
type: TextContentQueries.byTenant,
|
|
163
|
+
payload: tenantIdOverride !== undefined ? { tenantIdOverride } : {},
|
|
164
|
+
}),
|
|
164
165
|
})
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
166
|
+
.then(async (r) => {
|
|
167
|
+
if (!r.ok) {
|
|
168
|
+
// 403 (CSRF/auth) oder 5xx — explicit error statt silent empty.
|
|
169
|
+
const text = await r.text().catch(() => r.statusText);
|
|
170
|
+
throw new Error(`text-content load failed: ${r.status} ${text}`);
|
|
171
|
+
}
|
|
172
|
+
return r.json();
|
|
173
|
+
})
|
|
174
|
+
.then((data: ByTenantResponse) => {
|
|
175
|
+
// Der App-seitige r.nav-Knoten IST der "Content"-Container → die
|
|
176
|
+
// Provider-Kinder sind die Folder/Leaves darunter, nicht der Wrapper.
|
|
177
|
+
const content = groupBlocksByFolder(data.data.blocks, tenantIdOverride)[0];
|
|
178
|
+
emit(content !== undefined && Array.isArray(content.children) ? content.children : []);
|
|
179
|
+
})
|
|
180
|
+
.catch((e) => {
|
|
181
|
+
// V.1.4: explicit error-Signal via emitError. ProviderBranch zeigt
|
|
182
|
+
// Banner + Retry-Button. Fallback auf emit([]) wenn der Consumer
|
|
183
|
+
// kein emitError unterstützt (Tests etc.).
|
|
184
|
+
if (emitError) {
|
|
185
|
+
emitError(e instanceof Error ? e : new Error(String(e)));
|
|
186
|
+
} else {
|
|
187
|
+
emit([]);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return () => {};
|
|
191
|
+
};
|
|
192
|
+
}
|
|
177
193
|
|
|
178
194
|
// V.1.3 echte Edit-Form: lädt aktuelle Werte via by-slug-query, lässt
|
|
179
195
|
// TenantAdmin/SystemAdmin title + body editieren, dispatcht set-write
|
|
@@ -202,9 +218,11 @@ type SetResponse = { readonly slug: string; readonly lang: string; readonly isNe
|
|
|
202
218
|
|
|
203
219
|
function TextContentEditor({
|
|
204
220
|
target,
|
|
205
|
-
onClose,
|
|
206
221
|
}: {
|
|
207
222
|
readonly target: TargetRef;
|
|
223
|
+
// onClose bleibt Teil des Resolver-Contracts (Visual-Panel liefert es),
|
|
224
|
+
// wird aber nicht mehr gebraucht: die Tree-Auswahl navigiert, kein
|
|
225
|
+
// Schließen-Button mehr. Nicht destrukturiert → kein unused binding.
|
|
208
226
|
readonly onClose: () => void;
|
|
209
227
|
}): ReactNode {
|
|
210
228
|
// @cast-boundary visual-tree-args — TargetRef.args ist erased zu
|
|
@@ -213,9 +231,12 @@ function TextContentEditor({
|
|
|
213
231
|
// zu Event-Payloads. Optional-Chain absorbiert fehlende Felder ohne
|
|
214
232
|
// throw, damit der Editor auch bei manuellem URL-Tampering nicht
|
|
215
233
|
// crasht (TargetRef könnte aus old localStorage / URL-State stammen).
|
|
216
|
-
const args = target.args as
|
|
234
|
+
const args = target.args as
|
|
235
|
+
| { slug?: string; lang?: string; tenantIdOverride?: string }
|
|
236
|
+
| undefined;
|
|
217
237
|
const slug = args?.slug ?? "";
|
|
218
238
|
const lang = args?.lang ?? "";
|
|
239
|
+
const tenantIdOverride = args?.tenantIdOverride;
|
|
219
240
|
|
|
220
241
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
221
242
|
const dispatcher = useDispatcher();
|
|
@@ -232,7 +253,7 @@ function TextContentEditor({
|
|
|
232
253
|
error: loadError,
|
|
233
254
|
} = useQuery<TextBlock | null>(
|
|
234
255
|
TextContentQueries.bySlug,
|
|
235
|
-
{ slug, lang },
|
|
256
|
+
{ slug, lang, ...(tenantIdOverride !== undefined && { tenantIdOverride }) },
|
|
236
257
|
{ enabled: slug !== "" && lang !== "" },
|
|
237
258
|
);
|
|
238
259
|
|
|
@@ -266,6 +287,7 @@ function TextContentEditor({
|
|
|
266
287
|
lang,
|
|
267
288
|
title,
|
|
268
289
|
body: body.length > 0 ? body : null,
|
|
290
|
+
...(tenantIdOverride !== undefined && { tenantIdOverride }),
|
|
269
291
|
});
|
|
270
292
|
if (result.isSuccess) {
|
|
271
293
|
setSavedMsg(result.data.isNew ? "Neu angelegt." : "Gespeichert.");
|
|
@@ -290,61 +312,53 @@ function TextContentEditor({
|
|
|
290
312
|
const disabled = submitting || loading || !canWrite;
|
|
291
313
|
|
|
292
314
|
return (
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{canWrite && (
|
|
341
|
-
<Button type="submit" loading={submitting} disabled={disabled}>
|
|
342
|
-
{submitting ? "Speichern…" : "Speichern"}
|
|
343
|
-
</Button>
|
|
344
|
-
)}
|
|
345
|
-
</Form>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
315
|
+
<Form
|
|
316
|
+
onSubmit={onSubmit}
|
|
317
|
+
testId="text-content-editor"
|
|
318
|
+
title={title || slug || "—"}
|
|
319
|
+
subtitle={lang !== "" ? `(${lang})` : undefined}
|
|
320
|
+
actions={
|
|
321
|
+
canWrite ? (
|
|
322
|
+
<Button type="submit" loading={submitting} disabled={disabled}>
|
|
323
|
+
{submitting ? "Speichern…" : "Speichern"}
|
|
324
|
+
</Button>
|
|
325
|
+
) : undefined
|
|
326
|
+
}
|
|
327
|
+
>
|
|
328
|
+
{loading && <Banner variant="loading">Lädt aktuellen Stand…</Banner>}
|
|
329
|
+
{loadError !== null && (
|
|
330
|
+
<Banner variant="error">Konnte Block nicht laden: {loadError.code}</Banner>
|
|
331
|
+
)}
|
|
332
|
+
{!canWrite && !loading && (
|
|
333
|
+
<Banner variant="info">
|
|
334
|
+
Read-only — TenantAdmin- oder SystemAdmin-Rolle für Änderungen erforderlich.
|
|
335
|
+
</Banner>
|
|
336
|
+
)}
|
|
337
|
+
<Field id="text-content-title" label="Titel" required>
|
|
338
|
+
<Input
|
|
339
|
+
kind="text"
|
|
340
|
+
id="text-content-title"
|
|
341
|
+
name="text-content-title"
|
|
342
|
+
value={title}
|
|
343
|
+
onChange={setTitle}
|
|
344
|
+
disabled={disabled}
|
|
345
|
+
required
|
|
346
|
+
/>
|
|
347
|
+
</Field>
|
|
348
|
+
<Field id="text-content-body" label="Inhalt">
|
|
349
|
+
<Input
|
|
350
|
+
kind="textarea"
|
|
351
|
+
id="text-content-body"
|
|
352
|
+
name="text-content-body"
|
|
353
|
+
value={body}
|
|
354
|
+
onChange={setBody}
|
|
355
|
+
disabled={disabled}
|
|
356
|
+
rows={14}
|
|
357
|
+
/>
|
|
358
|
+
</Field>
|
|
359
|
+
{saveError !== null && <Banner variant="error">{saveError}</Banner>}
|
|
360
|
+
{savedMsg !== null && <Banner variant="info">{savedMsg}</Banner>}
|
|
361
|
+
</Form>
|
|
348
362
|
);
|
|
349
363
|
}
|
|
350
364
|
|
|
@@ -353,12 +367,18 @@ function TextContentEditor({
|
|
|
353
367
|
// Label/Icon/Access des Knotens (managed-pages-Konvention) — das Feature
|
|
354
368
|
// liefert nur die Kinder + den Editor. Ohne navId: nur der Resolver, kein
|
|
355
369
|
// Sidebar-Knoten (server-only-Consumer wie money-horse leaken nichts).
|
|
356
|
-
|
|
370
|
+
// `tenantId` (SystemAdmin-only): welchen Tenant der Content-Tree + Editor
|
|
371
|
+
// bedienen — SYSTEM_TENANT_ID für Apps, die Marketing/Legal dort seeden.
|
|
372
|
+
// Weglassen → Session-Tenant (Default; kein Cross-Tenant-Zugriff).
|
|
373
|
+
export function textContentClient(opts?: {
|
|
374
|
+
readonly navId?: string;
|
|
375
|
+
readonly tenantId?: string;
|
|
376
|
+
}): ClientFeatureDefinition {
|
|
357
377
|
const navId = opts?.navId;
|
|
358
378
|
return {
|
|
359
379
|
name: "text-content",
|
|
360
380
|
...(navId !== undefined && {
|
|
361
|
-
navProviders: { [navId]:
|
|
381
|
+
navProviders: { [navId]: makeTreeProvider(opts?.tenantId) },
|
|
362
382
|
// SSE-Refresh: jedes text-block-Event re-fired den Provider → Tree live.
|
|
363
383
|
navEntities: { [navId]: ["text-block"] },
|
|
364
384
|
}),
|
|
@@ -93,8 +93,6 @@ export function TierAdminScreen(): ReactNode {
|
|
|
93
93
|
|
|
94
94
|
return (
|
|
95
95
|
<FormScreenShell testId="tier-admin-screen" className="flex flex-col gap-6">
|
|
96
|
-
<Text variant="small">{t("tier-admin.explainer")}</Text>
|
|
97
|
-
|
|
98
96
|
{tenantsQuery.error !== null && (
|
|
99
97
|
<Banner variant="error" testId="tier-admin-load-error">
|
|
100
98
|
{t("tier-admin.error.load")}
|
|
@@ -123,6 +121,8 @@ export function TierAdminScreen(): ReactNode {
|
|
|
123
121
|
</Button>
|
|
124
122
|
}
|
|
125
123
|
>
|
|
124
|
+
<Text variant="small">{t("tier-admin.explainer")}</Text>
|
|
125
|
+
|
|
126
126
|
<Field id="tier-admin-tenant" label={t("tier-admin.tenant.label")} required>
|
|
127
127
|
<Input
|
|
128
128
|
kind="select"
|
package/src/user/constants.ts
CHANGED
|
@@ -267,12 +267,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
267
267
|
access: { roles: ["SystemAdmin"] },
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
-
r.translations({
|
|
271
|
-
keys: {
|
|
272
|
-
...USER_DATA_RIGHTS_I18N,
|
|
273
|
-
"user-data-rights:nav.exportJobs": { de: "DSGVO-Exporte", en: "GDPR exports" },
|
|
274
|
-
},
|
|
275
|
-
});
|
|
270
|
+
r.translations({ keys: USER_DATA_RIGHTS_I18N });
|
|
276
271
|
|
|
277
272
|
// Dormant Self-Service-Screen (Art. 15/17/18/20): Export, Aktivitäts-
|
|
278
273
|
// protokoll, Einschränkung, Löschung in einem Screen. Kein r.nav — die
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Server + client i18n for user-data-rights (operator screens + nav). Pure
|
|
3
|
+
// data, importable from the web bundle (web/i18n.ts derives the client keys).
|
|
1
4
|
type LocalizedString = { readonly de: string; readonly en: string };
|
|
2
5
|
|
|
3
6
|
export const USER_DATA_RIGHTS_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
+
"user-data-rights:nav.exportJobs": { de: "DSGVO-Exporte", en: "GDPR exports" },
|
|
4
8
|
"screen:export-job-list.title": { de: "DSGVO-Exporte", en: "GDPR exports" },
|
|
5
9
|
"screen:export-job-detail.title": { de: "Export-Job", en: "Export job" },
|
|
6
10
|
"screen:download-attempt-list.title": { de: "Download-Versuche", en: "Download attempts" },
|
|
@@ -5,8 +5,24 @@
|
|
|
5
5
|
// deletion.<step>.<slug>`.
|
|
6
6
|
|
|
7
7
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
8
|
+
import { USER_DATA_RIGHTS_I18N } from "../i18n";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const LOCALES = ["de", "en"] as const;
|
|
11
|
+
|
|
12
|
+
// Operator keys (export-job list/detail titles + entity field labels + nav)
|
|
13
|
+
// live in the server i18n; the SystemAdmin entityList screens render them
|
|
14
|
+
// client-side, so the web bundle must carry them too — otherwise raw keys
|
|
15
|
+
// show. Derive from the single server source instead of duplicating.
|
|
16
|
+
const operatorTranslations: TranslationsByLocale = Object.fromEntries(
|
|
17
|
+
LOCALES.map((locale) => [
|
|
18
|
+
locale,
|
|
19
|
+
Object.fromEntries(
|
|
20
|
+
Object.entries(USER_DATA_RIGHTS_I18N).map(([key, value]) => [key, value[locale]]),
|
|
21
|
+
),
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const apexTranslations: TranslationsByLocale = {
|
|
10
26
|
de: {
|
|
11
27
|
"userDataRights.deletion.request.title": "Account-Löschung beantragen",
|
|
12
28
|
"userDataRights.deletion.request.intro":
|
|
@@ -152,3 +168,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
152
168
|
"The download is currently unavailable due to a server configuration issue. The operator has been notified.",
|
|
153
169
|
},
|
|
154
170
|
};
|
|
171
|
+
|
|
172
|
+
export const defaultTranslations: TranslationsByLocale = {
|
|
173
|
+
de: { ...apexTranslations["de"], ...operatorTranslations["de"] },
|
|
174
|
+
en: { ...apexTranslations["en"], ...operatorTranslations["en"] },
|
|
175
|
+
};
|