@cosmicdrift/kumiko-bundled-features 0.205.0 → 0.207.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/package.json +7 -7
- package/src/admin-shell/i18n.ts +36 -17
- package/src/admin-shell/web/i18n.ts +1 -1
- package/src/audit/i18n.ts +41 -25
- package/src/audit/web/i18n.ts +1 -1
- package/src/auth-email-password/i18n.ts +129 -0
- package/src/auth-mfa/i18n.ts +2 -1
- package/src/auth-mfa/web/i18n.ts +63 -0
- package/src/cap-counter/i18n.ts +10 -5
- package/src/compliance-profiles/i18n.ts +11 -3
- package/src/compliance-profiles/web/i18n.ts +10 -0
- package/src/config/i18n.ts +5 -5
- package/src/config/web/i18n.ts +9 -0
- package/src/custom-fields/web/i18n.ts +9 -0
- package/src/delivery/i18n.ts +7 -3
- package/src/delivery/web/i18n.ts +10 -0
- package/src/feature-toggles/i18n.ts +11 -3
- package/src/feature-toggles/web/i18n.ts +12 -0
- package/src/folders/web/i18n.ts +25 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +11 -10
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +21 -26
- package/src/jobs/i18n.ts +62 -40
- package/src/jobs/web/i18n.ts +1 -1
- package/src/managed-pages/i18n.ts +63 -23
- package/src/managed-pages/web/i18n.ts +1 -1
- package/src/notes-history/i18n.ts +1 -1
- package/src/notes-history/web/i18n.ts +8 -0
- package/src/personal-access-tokens/i18n.ts +6 -2
- package/src/personal-access-tokens/web/i18n.ts +35 -0
- package/src/sessions/i18n.ts +21 -17
- package/src/tags/i18n.ts +2 -2
- package/src/tags/web/i18n.ts +25 -0
- package/src/template-resolver/web/i18n.ts +14 -0
- package/src/tenant/i18n.ts +24 -13
- package/src/tenant/web/i18n.ts +19 -0
- package/src/tier-engine/i18n.ts +22 -2
- package/src/user/__tests__/email-unique-blind-index.integration.test.ts +140 -0
- package/src/user/__tests__/user.integration.test.ts +31 -0
- package/src/user/handlers/create.write.ts +54 -8
- package/src/user/i18n.ts +31 -14
- package/src/user/schema/user.ts +11 -0
- package/src/user-data-rights/i18n.ts +66 -18
- package/src/user-data-rights/web/i18n.ts +70 -1
- package/src/user-profile/i18n.ts +35 -0
|
@@ -323,11 +323,11 @@ describe("scenario 2b: ingest-message — Thread-Rollup selbstkorrigierend statt
|
|
|
323
323
|
});
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
describe("scenario 2c: ingest-message — concurrent thread-rollup race (issue #1229)", () => {
|
|
326
|
+
describe("scenario 2c: ingest-message — concurrent thread-rollup race (issue #1229, closed by #2155)", () => {
|
|
327
327
|
// Probabilistic — looped per house convention (mehrere Durchläufe, z.B.
|
|
328
|
-
// 20x):
|
|
329
|
-
//
|
|
330
|
-
// a single run can't force deterministically.
|
|
328
|
+
// 20x): whether a concurrent thread-rollup commit lands inside the
|
|
329
|
+
// step-5 critical section depends on real transaction-timing
|
|
330
|
+
// interleaving, which a single run can't force deterministically.
|
|
331
331
|
const ITERATIONS = 20;
|
|
332
332
|
const CONCURRENCY = 3;
|
|
333
333
|
|
|
@@ -352,10 +352,10 @@ describe("scenario 2c: ingest-message — concurrent thread-rollup race (issue #
|
|
|
352
352
|
),
|
|
353
353
|
);
|
|
354
354
|
|
|
355
|
-
// No writer's transaction fails with a 500
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
355
|
+
// No writer's transaction fails with a 500 — each of the CONCURRENCY
|
|
356
|
+
// distinct messages is its own aggregate, so step 4 never races; step
|
|
357
|
+
// 5 (shared threadAggId) is serialized by the pg_advisory_xact_lock,
|
|
358
|
+
// so there's nothing here for the bounded retry loop to absorb.
|
|
359
359
|
for (const res of responses) {
|
|
360
360
|
expect(res.status).toBe(200);
|
|
361
361
|
const body = (await res.json()) as { isSuccess: boolean; data: { duplicate: boolean } };
|
|
@@ -366,8 +366,9 @@ describe("scenario 2c: ingest-message — concurrent thread-rollup race (issue #
|
|
|
366
366
|
const threadAggId = mailThreadAggregateId(admin.tenantId, `mid:${rootMessageIdHeader}`);
|
|
367
367
|
const threadRows = await selectMany(db, mailThreadsProjectionTable, { id: threadAggId });
|
|
368
368
|
expect(threadRows).toHaveLength(1);
|
|
369
|
-
// Every message's contribution landed —
|
|
370
|
-
//
|
|
369
|
+
// Every message's contribution landed — the advisory lock closes the
|
|
370
|
+
// TOCTOU gap between countWhere and append that used to let a stale
|
|
371
|
+
// messageCount succeed without ever surfacing as a version conflict.
|
|
371
372
|
expect(threadRows[0]?.["messageCount"]).toBe(CONCURRENCY);
|
|
372
373
|
}
|
|
373
374
|
});
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
configuredPiiSubjectKms,
|
|
26
26
|
encryptPiiFieldValues,
|
|
27
27
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
28
|
+
import { acquireNamespacedAdvisoryLock } from "@cosmicdrift/kumiko-framework/db";
|
|
28
29
|
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
29
30
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
30
31
|
import { Temporal } from "temporal-polyfill";
|
|
@@ -101,6 +102,10 @@ function buildThreadKey(p: IngestMessagePayload, effectiveMessageIdHeader: strin
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const THREAD_ROLLUP_MAX_ATTEMPTS = 5;
|
|
105
|
+
// pg_advisory_xact_lock namespace (int4) for the step-5 thread-rollup lock
|
|
106
|
+
// — 'inbm' as ASCII, keeps it disjoint from the framework's other fixed
|
|
107
|
+
// single-int advisory-lock keys (schema bootstrap, es-ops boot).
|
|
108
|
+
const THREAD_ROLLUP_LOCK_NAMESPACE = 0x696e626d;
|
|
104
109
|
export const ingestMessageHandler: WriteHandlerDef = {
|
|
105
110
|
name: "ingest-message",
|
|
106
111
|
schema: ingestMessageSchema,
|
|
@@ -230,35 +235,23 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
230
235
|
// encrypted once up front (constant across retries, doesn't
|
|
231
236
|
// depend on the thread's prior state).
|
|
232
237
|
//
|
|
233
|
-
// messageCount
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// stale row-count this countWhere captured before the concurrent
|
|
247
|
-
// insert. That write still lands (messageCount can undercount by
|
|
248
|
-
// one round), it is not eliminated by this retry loop; the next
|
|
249
|
-
// ingest on the thread re-counts and self-corrects. Unlike step 4,
|
|
250
|
-
// a lost version-conflict race here must NOT be treated as a no-op
|
|
238
|
+
// messageCount is a live COUNT, not previousCount+1, so two
|
|
239
|
+
// concurrent ingests on the same thread (Watch-Push vs.
|
|
240
|
+
// Poll-Reconciliation overlap, or two different messages of the
|
|
241
|
+
// same thread arriving at once) converge on the true count.
|
|
242
|
+
// acquireNamespacedAdvisoryLock below (issue #2155) serializes this whole
|
|
243
|
+
// countWhere+append section per threadAggId — transaction-scoped,
|
|
244
|
+
// so it survives tryAppendEvent's inner SAVEPOINT and releases at
|
|
245
|
+
// the enclosing TX's commit/rollback. Without it, a concurrent
|
|
246
|
+
// commit landing between countWhere and tryAppendEvent's own fresh
|
|
247
|
+
// version-read makes the append succeed with a stale count instead
|
|
248
|
+
// of a VersionConflictError, so the retry loop below never sees it
|
|
249
|
+
// as a conflict to retry (#1229's original gap). Unlike step 4, a
|
|
250
|
+
// lost version-conflict race here must NOT be treated as a no-op
|
|
251
251
|
// duplicate — skipping would leave this message's contribution to
|
|
252
252
|
// messageCount/lastMessageAt out of the thread forever. Exhausting
|
|
253
253
|
// all attempts throws, rolling back the whole TX (including the
|
|
254
|
-
// step-4 message append) for a clean re-ingest
|
|
255
|
-
// never return success with a stale snapshot.
|
|
256
|
-
//
|
|
257
|
-
// A true race-free read+append (e.g. SELECT ... FOR UPDATE or an
|
|
258
|
-
// advisory lock on threadAggId) would close the countWhere→append
|
|
259
|
-
// gap above and is deliberately not part of this fix — two writers
|
|
260
|
-
// (watch + poll) make self-correcting drift an acceptable trade for
|
|
261
|
-
// now.
|
|
254
|
+
// step-4 message append) for a clean re-ingest.
|
|
262
255
|
// ---------------------------------------------------------------
|
|
263
256
|
const threadPlainPii = { tenantId, subject: payload.subject };
|
|
264
257
|
const encryptedThreadFields = piiKms
|
|
@@ -274,6 +267,8 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
274
267
|
)
|
|
275
268
|
: threadPlainPii;
|
|
276
269
|
|
|
270
|
+
await acquireNamespacedAdvisoryLock(ctx.db.raw, THREAD_ROLLUP_LOCK_NAMESPACE, threadAggId);
|
|
271
|
+
|
|
277
272
|
let threadAppendOk = false;
|
|
278
273
|
for (let attempt = 1; attempt <= THREAD_ROLLUP_MAX_ATTEMPTS && !threadAppendOk; attempt++) {
|
|
279
274
|
const threadEvents = await ctx.loadAggregate(threadAggId);
|
package/src/jobs/i18n.ts
CHANGED
|
@@ -1,70 +1,92 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Server + client i18n for jobs operator screens.
|
|
3
3
|
|
|
4
|
-
type LocalizedString = { readonly de: string; readonly en: string };
|
|
4
|
+
type LocalizedString = { readonly de: string; readonly en: string; readonly es: string };
|
|
5
5
|
|
|
6
6
|
export const JOBS_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
-
"screen:job-runs.title": { de: "Job-Läufe", en: "Job runs" },
|
|
8
|
-
"screen:job-run-detail.title": { de: "Job-Lauf", en: "Job run" },
|
|
9
|
-
"jobs:nav.jobRuns": { de: "Jobs", en: "Jobs" },
|
|
10
|
-
"jobs.runs.title": { de: "Job-Läufe", en: "Job runs" },
|
|
11
|
-
"jobs.runs.loading": { de: "Lade Läufe…", en: "Loading runs…" },
|
|
12
|
-
"jobs.runs.empty": {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"jobs.runs.
|
|
18
|
-
"jobs.runs.filter.
|
|
19
|
-
"jobs.runs.filter.
|
|
20
|
-
"jobs.runs.
|
|
21
|
-
"jobs.runs.
|
|
22
|
-
"jobs.runs.
|
|
23
|
-
"jobs.runs.
|
|
24
|
-
"jobs.
|
|
25
|
-
"jobs.
|
|
26
|
-
"jobs.
|
|
27
|
-
"jobs.
|
|
28
|
-
"jobs.detail.
|
|
29
|
-
"jobs.detail.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"jobs.detail.
|
|
35
|
-
"jobs.detail.
|
|
36
|
-
"jobs.detail.
|
|
37
|
-
"jobs.
|
|
7
|
+
"screen:job-runs.title": { de: "Job-Läufe", en: "Job runs", es: "Ejecuciones de trabajos" },
|
|
8
|
+
"screen:job-run-detail.title": { de: "Job-Lauf", en: "Job run", es: "Ejecución de trabajo" },
|
|
9
|
+
"jobs:nav.jobRuns": { de: "Jobs", en: "Jobs", es: "Trabajos" },
|
|
10
|
+
"jobs.runs.title": { de: "Job-Läufe", en: "Job runs", es: "Ejecuciones de trabajos" },
|
|
11
|
+
"jobs.runs.loading": { de: "Lade Läufe…", en: "Loading runs…", es: "Cargando ejecuciones…" },
|
|
12
|
+
"jobs.runs.empty": {
|
|
13
|
+
de: "Keine Job-Läufe.",
|
|
14
|
+
en: "No job runs.",
|
|
15
|
+
es: "No hay ejecuciones de trabajos.",
|
|
16
|
+
},
|
|
17
|
+
"jobs.runs.open": { de: "Details", en: "Details", es: "Detalles" },
|
|
18
|
+
"jobs.runs.filter.status": { de: "Status", en: "Status", es: "Estado" },
|
|
19
|
+
"jobs.runs.filter.all": { de: "Alle", en: "All", es: "Todos" },
|
|
20
|
+
"jobs.runs.filter.completed": { de: "Abgeschlossen", en: "Completed", es: "Completado" },
|
|
21
|
+
"jobs.runs.filter.failed": { de: "Fehlgeschlagen", en: "Failed", es: "Fallido" },
|
|
22
|
+
"jobs.runs.filter.running": { de: "Läuft", en: "Running", es: "En curso" },
|
|
23
|
+
"jobs.runs.filter.queued": { de: "Wartend", en: "Queued", es: "En cola" },
|
|
24
|
+
"jobs.runs.col.job": { de: "Job", en: "Job", es: "Trabajo" },
|
|
25
|
+
"jobs.runs.col.status": { de: "Status", en: "Status", es: "Estado" },
|
|
26
|
+
"jobs.runs.col.started": { de: "Gestartet", en: "Started", es: "Iniciado" },
|
|
27
|
+
"jobs.runs.col.duration": { de: "Dauer (ms)", en: "Duration (ms)", es: "Duración (ms)" },
|
|
28
|
+
"jobs.detail.loading": { de: "Lade Details…", en: "Loading details…", es: "Cargando detalles…" },
|
|
29
|
+
"jobs.detail.missing": {
|
|
30
|
+
de: "Lauf nicht gefunden.",
|
|
31
|
+
en: "Run not found.",
|
|
32
|
+
es: "Ejecución no encontrada.",
|
|
33
|
+
},
|
|
34
|
+
"jobs.detail.field.job": { de: "Job", en: "Job", es: "Trabajo" },
|
|
35
|
+
"jobs.detail.field.status": { de: "Status", en: "Status", es: "Estado" },
|
|
36
|
+
"jobs.detail.field.id": { de: "Run-ID", en: "Run ID", es: "ID de ejecución" },
|
|
37
|
+
"jobs.detail.field.started": { de: "Gestartet", en: "Started", es: "Iniciado" },
|
|
38
|
+
"jobs.detail.field.finished": { de: "Beendet", en: "Finished", es: "Finalizado" },
|
|
39
|
+
"jobs.detail.field.duration": { de: "Dauer (ms)", en: "Duration (ms)", es: "Duración (ms)" },
|
|
40
|
+
"jobs.detail.field.error": { de: "Fehler", en: "Error", es: "Error" },
|
|
41
|
+
"jobs.detail.logs": { de: "Logs", en: "Logs", es: "Registros" },
|
|
42
|
+
"jobs.detail.logs.empty": {
|
|
43
|
+
de: "Keine Log-Zeilen.",
|
|
44
|
+
en: "No log lines.",
|
|
45
|
+
es: "No hay líneas de registro.",
|
|
46
|
+
},
|
|
47
|
+
"jobs.detail.retry": { de: "Erneut ausführen", en: "Retry", es: "Reintentar" },
|
|
48
|
+
"jobs.detail.retrying": { de: "Wird erneut gestartet…", en: "Retrying…", es: "Reintentando…" },
|
|
49
|
+
"jobs.trigger.title": { de: "Job starten", en: "Run a job", es: "Ejecutar un trabajo" },
|
|
38
50
|
"jobs.trigger.explainer": {
|
|
39
51
|
de: "Nur manuell startbare Jobs. Nach dem Start erscheint der Lauf in der Historie unten.",
|
|
40
52
|
en: "Only manually triggerable jobs. After starting, the run appears in the history below.",
|
|
53
|
+
es: "Solo trabajos que se pueden iniciar manualmente. Tras iniciarlo, la ejecución aparece en el historial de abajo.",
|
|
41
54
|
},
|
|
42
|
-
"jobs.trigger.job": { de: "Job", en: "Job" },
|
|
43
|
-
"jobs.trigger.payload": { de: "Payload (JSON)", en: "Payload (JSON)" },
|
|
44
|
-
"jobs.trigger.payload.hint": { de: "Schema-Hinweis", en: "Schema hint" },
|
|
55
|
+
"jobs.trigger.job": { de: "Job", en: "Job", es: "Trabajo" },
|
|
56
|
+
"jobs.trigger.payload": { de: "Payload (JSON)", en: "Payload (JSON)", es: "Payload (JSON)" },
|
|
57
|
+
"jobs.trigger.payload.hint": { de: "Schema-Hinweis", en: "Schema hint", es: "Pista de esquema" },
|
|
45
58
|
"jobs.trigger.payload.invalidJson": {
|
|
46
59
|
de: "Payload ist kein gültiges JSON-Objekt.",
|
|
47
60
|
en: "Payload is not a valid JSON object.",
|
|
61
|
+
es: "El payload no es un objeto JSON válido.",
|
|
48
62
|
},
|
|
49
|
-
"jobs.trigger.submit": { de: "Starten", en: "Run" },
|
|
50
|
-
"jobs.trigger.submitting": { de: "Wird gestartet…", en: "Starting…" },
|
|
51
|
-
"jobs.trigger.success": { de: "Job gestartet.", en: "Job started." },
|
|
63
|
+
"jobs.trigger.submit": { de: "Starten", en: "Run", es: "Ejecutar" },
|
|
64
|
+
"jobs.trigger.submitting": { de: "Wird gestartet…", en: "Starting…", es: "Iniciando…" },
|
|
65
|
+
"jobs.trigger.success": { de: "Job gestartet.", en: "Job started.", es: "Trabajo iniciado." },
|
|
52
66
|
"jobs.trigger.empty": {
|
|
53
67
|
de: "Keine manuell startbaren Jobs registriert.",
|
|
54
68
|
en: "No manually triggerable jobs are registered.",
|
|
69
|
+
es: "No hay trabajos registrados que se puedan iniciar manualmente.",
|
|
55
70
|
},
|
|
56
71
|
"jobs.trigger.perTenant": {
|
|
57
72
|
de: "Läuft einmal pro aktivem Tenant.",
|
|
58
73
|
en: "Runs once per active tenant.",
|
|
74
|
+
es: "Se ejecuta una vez por cada organización activa.",
|
|
75
|
+
},
|
|
76
|
+
"jobs.errors.unknownJob": {
|
|
77
|
+
de: "Unbekannter Job.",
|
|
78
|
+
en: "Unknown job.",
|
|
79
|
+
es: "Trabajo desconocido.",
|
|
59
80
|
},
|
|
60
|
-
"jobs.errors.unknownJob": { de: "Unbekannter Job.", en: "Unknown job." },
|
|
61
81
|
"jobs.errors.notManual": {
|
|
62
82
|
de: "Dieser Job ist nicht manuell startbar.",
|
|
63
83
|
en: "This job cannot be triggered manually.",
|
|
84
|
+
es: "Este trabajo no se puede iniciar manualmente.",
|
|
64
85
|
},
|
|
65
|
-
"jobs.errors.notFound": { de: "Nicht gefunden.", en: "Not found." },
|
|
86
|
+
"jobs.errors.notFound": { de: "Nicht gefunden.", en: "Not found.", es: "No encontrado." },
|
|
66
87
|
"jobs.errors.onlyFailedCanRetry": {
|
|
67
88
|
de: "Nur fehlgeschlagene Läufe können erneut gestartet werden.",
|
|
68
89
|
en: "Only failed runs can be retried.",
|
|
90
|
+
es: "Solo se pueden reintentar las ejecuciones fallidas.",
|
|
69
91
|
},
|
|
70
92
|
};
|
package/src/jobs/web/i18n.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import { JOBS_I18N } from "../i18n";
|
|
4
4
|
|
|
5
|
-
const LOCALES = ["de", "en"] as const;
|
|
5
|
+
const LOCALES = ["de", "en", "es"] as const;
|
|
6
6
|
|
|
7
7
|
export const defaultTranslations: TranslationsByLocale = Object.fromEntries(
|
|
8
8
|
LOCALES.map((locale) => [
|
|
@@ -11,56 +11,96 @@
|
|
|
11
11
|
// registriert. Die zwei customCss-Keys werden unconditional geliefert (schaden
|
|
12
12
|
// bei allowCustomCss:false nicht, sind aber Pflicht für Consumer die es aktivieren).
|
|
13
13
|
|
|
14
|
-
type LocalizedString = { readonly de: string; readonly en: string };
|
|
14
|
+
type LocalizedString = { readonly de: string; readonly en: string; readonly es: string };
|
|
15
15
|
|
|
16
16
|
export const MANAGED_PAGES_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
17
17
|
// Screen-Titel (global/unpräfixiert — screenTitleKey = screen:<id>.title)
|
|
18
|
-
"screen:page-list.title": { de: "Seiten", en: "Pages" },
|
|
19
|
-
"screen:page-edit.title": { de: "Seite bearbeiten", en: "Edit page" },
|
|
20
|
-
"screen:branding-settings.title": { de: "Branding", en: "Branding" },
|
|
18
|
+
"screen:page-list.title": { de: "Seiten", en: "Pages", es: "Páginas" },
|
|
19
|
+
"screen:page-edit.title": { de: "Seite bearbeiten", en: "Edit page", es: "Editar página" },
|
|
20
|
+
"screen:branding-settings.title": { de: "Branding", en: "Branding", es: "Marca" },
|
|
21
21
|
|
|
22
22
|
// Page-Entity Feld-Labels (managed-pages:entity:page:field:*)
|
|
23
|
-
"managed-pages:entity:page:field:slug": { de: "Slug", en: "Slug" },
|
|
24
|
-
"managed-pages:entity:page:field:lang": { de: "Sprache", en: "Language" },
|
|
25
|
-
"managed-pages:entity:page:field:title": { de: "Titel", en: "Title" },
|
|
26
|
-
"managed-pages:entity:page:field:published": {
|
|
23
|
+
"managed-pages:entity:page:field:slug": { de: "Slug", en: "Slug", es: "Slug" },
|
|
24
|
+
"managed-pages:entity:page:field:lang": { de: "Sprache", en: "Language", es: "Idioma" },
|
|
25
|
+
"managed-pages:entity:page:field:title": { de: "Titel", en: "Title", es: "Título" },
|
|
26
|
+
"managed-pages:entity:page:field:published": {
|
|
27
|
+
de: "Veröffentlicht",
|
|
28
|
+
en: "Published",
|
|
29
|
+
es: "Publicado",
|
|
30
|
+
},
|
|
27
31
|
"managed-pages:entity:page:field:published:option:true": {
|
|
28
32
|
de: "Veröffentlicht",
|
|
29
33
|
en: "Published",
|
|
34
|
+
es: "Publicado",
|
|
35
|
+
},
|
|
36
|
+
"managed-pages:entity:page:field:published:option:false": {
|
|
37
|
+
de: "Entwurf",
|
|
38
|
+
en: "Draft",
|
|
39
|
+
es: "Borrador",
|
|
40
|
+
},
|
|
41
|
+
"managed-pages:entity:page:field:description": {
|
|
42
|
+
de: "Beschreibung",
|
|
43
|
+
en: "Description",
|
|
44
|
+
es: "Descripción",
|
|
30
45
|
},
|
|
31
|
-
"managed-pages:entity:page:field:
|
|
32
|
-
"managed-pages:entity:page:field:
|
|
33
|
-
"managed-pages:entity:page:field:ogImage": { de: "OG-Bild", en: "OG image" },
|
|
34
|
-
"managed-pages:entity:page:field:body": { de: "Inhalt", en: "Content" },
|
|
46
|
+
"managed-pages:entity:page:field:ogImage": { de: "OG-Bild", en: "OG image", es: "Imagen OG" },
|
|
47
|
+
"managed-pages:entity:page:field:body": { de: "Inhalt", en: "Content", es: "Contenido" },
|
|
35
48
|
|
|
36
49
|
// Config-Edit Feld-Labels (managed-pages:entity:__config-edit__:field:*, camelCase Form-Keys)
|
|
37
|
-
"managed-pages:entity:__config-edit__:field:title": {
|
|
50
|
+
"managed-pages:entity:__config-edit__:field:title": {
|
|
51
|
+
de: "Website-Titel",
|
|
52
|
+
en: "Site title",
|
|
53
|
+
es: "Título del sitio",
|
|
54
|
+
},
|
|
38
55
|
"managed-pages:entity:__config-edit__:field:description": {
|
|
39
56
|
de: "Beschreibung",
|
|
40
57
|
en: "Description",
|
|
58
|
+
es: "Descripción",
|
|
59
|
+
},
|
|
60
|
+
"managed-pages:entity:__config-edit__:field:siteUrl": {
|
|
61
|
+
de: "Website-URL",
|
|
62
|
+
en: "Site URL",
|
|
63
|
+
es: "URL del sitio",
|
|
41
64
|
},
|
|
42
|
-
"managed-pages:entity:__config-edit__:field:siteUrl": { de: "Website-URL", en: "Site URL" },
|
|
43
65
|
"managed-pages:entity:__config-edit__:field:accentColor": {
|
|
44
66
|
de: "Akzentfarbe",
|
|
45
67
|
en: "Accent color",
|
|
68
|
+
es: "Color de acento",
|
|
69
|
+
},
|
|
70
|
+
"managed-pages:entity:__config-edit__:field:logoUrl": {
|
|
71
|
+
de: "Logo-URL",
|
|
72
|
+
en: "Logo URL",
|
|
73
|
+
es: "URL del logo",
|
|
74
|
+
},
|
|
75
|
+
"managed-pages:entity:__config-edit__:field:layoutPreset": {
|
|
76
|
+
de: "Layout",
|
|
77
|
+
en: "Layout",
|
|
78
|
+
es: "Diseño",
|
|
46
79
|
},
|
|
47
|
-
"managed-pages:entity:__config-edit__:field:logoUrl": { de: "Logo-URL", en: "Logo URL" },
|
|
48
|
-
"managed-pages:entity:__config-edit__:field:layoutPreset": { de: "Layout", en: "Layout" },
|
|
49
80
|
|
|
50
81
|
// Section-Header
|
|
51
|
-
"managed-pages:section.meta": { de: "Metadaten", en: "Metadata" },
|
|
52
|
-
"managed-pages:section.body": { de: "Inhalt", en: "Content" },
|
|
53
|
-
"managed-pages:branding.section.identity": { de: "Identität", en: "Identity" },
|
|
82
|
+
"managed-pages:section.meta": { de: "Metadaten", en: "Metadata", es: "Metadatos" },
|
|
83
|
+
"managed-pages:section.body": { de: "Inhalt", en: "Content", es: "Contenido" },
|
|
84
|
+
"managed-pages:branding.section.identity": { de: "Identität", en: "Identity", es: "Identidad" },
|
|
54
85
|
|
|
55
86
|
// Row-Actions + Confirm (pageListScreen)
|
|
56
|
-
"managed-pages:actions.edit": { de: "Bearbeiten", en: "Edit" },
|
|
57
|
-
"managed-pages:actions.delete": { de: "Löschen", en: "Delete" },
|
|
87
|
+
"managed-pages:actions.edit": { de: "Bearbeiten", en: "Edit", es: "Editar" },
|
|
88
|
+
"managed-pages:actions.delete": { de: "Löschen", en: "Delete", es: "Eliminar" },
|
|
58
89
|
"managed-pages:confirms.page-delete": {
|
|
59
90
|
de: "Diese Seite wirklich löschen?",
|
|
60
91
|
en: "Delete this page?",
|
|
92
|
+
es: "¿Seguro que quieres eliminar esta página?",
|
|
61
93
|
},
|
|
62
94
|
|
|
63
95
|
// Nur bei allowCustomCss:true im Screen — unconditional geliefert
|
|
64
|
-
"managed-pages:entity:__config-edit__:field:customCss": {
|
|
65
|
-
|
|
96
|
+
"managed-pages:entity:__config-edit__:field:customCss": {
|
|
97
|
+
de: "Eigenes CSS",
|
|
98
|
+
en: "Custom CSS",
|
|
99
|
+
es: "CSS personalizado",
|
|
100
|
+
},
|
|
101
|
+
"managed-pages:branding.section.custom-css": {
|
|
102
|
+
de: "Eigenes CSS",
|
|
103
|
+
en: "Custom CSS",
|
|
104
|
+
es: "CSS personalizado",
|
|
105
|
+
},
|
|
66
106
|
};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { MANAGED_PAGES_I18N } from "../i18n";
|
|
14
14
|
|
|
15
|
-
const LOCALES = ["de", "en"] as const;
|
|
15
|
+
const LOCALES = ["de", "en", "es"] as const;
|
|
16
16
|
|
|
17
17
|
export const defaultTranslations: TranslationsByLocale = Object.fromEntries(
|
|
18
18
|
LOCALES.map((locale) => [
|
|
@@ -23,4 +23,12 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
23
23
|
"notesHistory.section.add": "Add note",
|
|
24
24
|
"notesHistory.section.meta": "{author} · {date}",
|
|
25
25
|
},
|
|
26
|
+
es: {
|
|
27
|
+
"notesHistory.section.createMode": "Guarda primero la entrada para poder añadir notas.",
|
|
28
|
+
"notesHistory.section.loading": "Cargando…",
|
|
29
|
+
"notesHistory.section.empty": "Aún no hay notas.",
|
|
30
|
+
"notesHistory.section.working": "Guardando…",
|
|
31
|
+
"notesHistory.section.add": "Añadir nota",
|
|
32
|
+
"notesHistory.section.meta": "{author} · {date}",
|
|
33
|
+
},
|
|
26
34
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
type LocalizedString = { readonly de: string; readonly en: string };
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string; readonly es: string };
|
|
2
2
|
|
|
3
3
|
export const PAT_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
-
"screen:api-tokens.title": {
|
|
4
|
+
"screen:api-tokens.title": {
|
|
5
|
+
de: "Personal Access Tokens",
|
|
6
|
+
en: "Personal Access Tokens",
|
|
7
|
+
es: "Tokens de acceso personal",
|
|
8
|
+
},
|
|
5
9
|
};
|
|
@@ -74,4 +74,39 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
74
74
|
"pat.list.created": "Created {date}",
|
|
75
75
|
"pat.error.generic": "Action failed.",
|
|
76
76
|
},
|
|
77
|
+
es: {
|
|
78
|
+
"pat.title": "Tokens de acceso personal",
|
|
79
|
+
"pat.create.title": "Crear un token nuevo",
|
|
80
|
+
"pat.create.subtitle": "Elige los permisos y una caducidad. El token solo se muestra una vez.",
|
|
81
|
+
"pat.create.name": "Nombre",
|
|
82
|
+
"pat.create.namePlaceholder": "p. ej. automatización de CLI",
|
|
83
|
+
"pat.create.scopes": "Acceso por ámbito",
|
|
84
|
+
"pat.level.none": "Sin acceso",
|
|
85
|
+
"pat.level.read": "Lectura",
|
|
86
|
+
"pat.level.write": "Lectura y escritura",
|
|
87
|
+
"pat.create.expiry": "Válido hasta",
|
|
88
|
+
"pat.create.submit": "Crear token",
|
|
89
|
+
"pat.create.needName": "Indica un nombre.",
|
|
90
|
+
"pat.create.needScope": "Selecciona al menos un permiso.",
|
|
91
|
+
"pat.create.needPassword": "Introduce tu contraseña para confirmar.",
|
|
92
|
+
"pat.create.currentPassword": "Contraseña (para confirmar)",
|
|
93
|
+
"pat.create.mfaCode": "Código 2FA (si está activado)",
|
|
94
|
+
"pat.create.mfaCodePlaceholder": "Código de 6 dígitos",
|
|
95
|
+
"pat.expiry.30d": "30 días",
|
|
96
|
+
"pat.expiry.90d": "90 días",
|
|
97
|
+
"pat.expiry.1y": "1 año",
|
|
98
|
+
"pat.expiry.never": "Sin caducidad",
|
|
99
|
+
"pat.created.hint": "Copia este token ahora — por seguridad solo se muestra esta vez.",
|
|
100
|
+
"pat.created.copy": "Copiar",
|
|
101
|
+
"pat.created.copied": "Copiado ✓",
|
|
102
|
+
"pat.created.dismiss": "Listo",
|
|
103
|
+
"pat.list.title": "Tus tokens",
|
|
104
|
+
"pat.list.empty": "Todavía no hay tokens.",
|
|
105
|
+
"pat.list.revoke": "Revocar",
|
|
106
|
+
"pat.list.revoked": "Revocado",
|
|
107
|
+
"pat.list.validUntil": "Válido hasta {date}",
|
|
108
|
+
"pat.list.neverExpires": "Sin caducidad",
|
|
109
|
+
"pat.list.created": "Creado el {date}",
|
|
110
|
+
"pat.error.generic": "La acción ha fallado.",
|
|
111
|
+
},
|
|
77
112
|
};
|
package/src/sessions/i18n.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Server + client i18n for the sessions operator screens.
|
|
3
3
|
|
|
4
|
-
type LocalizedString = { readonly de: string; readonly en: string };
|
|
4
|
+
type LocalizedString = { readonly de: string; readonly en: string; readonly es: string };
|
|
5
5
|
|
|
6
6
|
export const SESSIONS_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
-
"screen:session-list.title": { de: "Sitzungen", en: "Sessions" },
|
|
8
|
-
"screen:session-detail.title": { de: "Sitzung", en: "Session" },
|
|
9
|
-
"sessions:nav.sessionList": { de: "Sitzungen", en: "Sessions" },
|
|
10
|
-
"sessions.list.col.id": { de: "ID", en: "ID" },
|
|
11
|
-
"sessions.list.col.userId": { de: "User-ID", en: "User ID" },
|
|
12
|
-
"sessions.list.col.createdAt": { de: "Erstellt", en: "Created" },
|
|
13
|
-
"sessions.list.col.expiresAt": { de: "Läuft ab", en: "Expires" },
|
|
14
|
-
"sessions.list.col.revokedAt": { de: "Widerrufen", en: "Revoked" },
|
|
15
|
-
"sessions.list.action.open": { de: "Details", en: "Details" },
|
|
16
|
-
"sessions.detail.field.id": { de: "ID", en: "ID" },
|
|
17
|
-
"sessions.detail.field.userId": { de: "User-ID", en: "User ID" },
|
|
18
|
-
"sessions.detail.field.createdAt": { de: "Erstellt", en: "Created" },
|
|
19
|
-
"sessions.detail.field.expiresAt": { de: "Läuft ab", en: "Expires" },
|
|
20
|
-
"sessions.detail.field.revokedAt": { de: "Widerrufen", en: "Revoked" },
|
|
21
|
-
"sessions.detail.field.ip": { de: "IP-Adresse", en: "IP address" },
|
|
22
|
-
"sessions.detail.field.userAgent": {
|
|
7
|
+
"screen:session-list.title": { de: "Sitzungen", en: "Sessions", es: "Sesiones" },
|
|
8
|
+
"screen:session-detail.title": { de: "Sitzung", en: "Session", es: "Sesión" },
|
|
9
|
+
"sessions:nav.sessionList": { de: "Sitzungen", en: "Sessions", es: "Sesiones" },
|
|
10
|
+
"sessions.list.col.id": { de: "ID", en: "ID", es: "ID" },
|
|
11
|
+
"sessions.list.col.userId": { de: "User-ID", en: "User ID", es: "ID de usuario" },
|
|
12
|
+
"sessions.list.col.createdAt": { de: "Erstellt", en: "Created", es: "Creado" },
|
|
13
|
+
"sessions.list.col.expiresAt": { de: "Läuft ab", en: "Expires", es: "Caduca" },
|
|
14
|
+
"sessions.list.col.revokedAt": { de: "Widerrufen", en: "Revoked", es: "Revocado" },
|
|
15
|
+
"sessions.list.action.open": { de: "Details", en: "Details", es: "Detalles" },
|
|
16
|
+
"sessions.detail.field.id": { de: "ID", en: "ID", es: "ID" },
|
|
17
|
+
"sessions.detail.field.userId": { de: "User-ID", en: "User ID", es: "ID de usuario" },
|
|
18
|
+
"sessions.detail.field.createdAt": { de: "Erstellt", en: "Created", es: "Creado" },
|
|
19
|
+
"sessions.detail.field.expiresAt": { de: "Läuft ab", en: "Expires", es: "Caduca" },
|
|
20
|
+
"sessions.detail.field.revokedAt": { de: "Widerrufen", en: "Revoked", es: "Revocado" },
|
|
21
|
+
"sessions.detail.field.ip": { de: "IP-Adresse", en: "IP address", es: "Dirección IP" },
|
|
22
|
+
"sessions.detail.field.userAgent": {
|
|
23
|
+
de: "User-Agent",
|
|
24
|
+
en: "User agent",
|
|
25
|
+
es: "Agente de usuario",
|
|
26
|
+
},
|
|
23
27
|
};
|
package/src/tags/i18n.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
type LocalizedString = { readonly de: string; readonly en: string };
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string; readonly es: string };
|
|
2
2
|
|
|
3
3
|
export const TAGS_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
-
"screen:tag-list.title": { de: "Tags", en: "Tags" },
|
|
4
|
+
"screen:tag-list.title": { de: "Tags", en: "Tags", es: "Etiquetas" },
|
|
5
5
|
};
|
package/src/tags/web/i18n.ts
CHANGED
|
@@ -57,4 +57,29 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
57
57
|
"tags.filter.label": "Filter by tag",
|
|
58
58
|
"tags.filter.clear": "Clear",
|
|
59
59
|
},
|
|
60
|
+
es: {
|
|
61
|
+
"tags.section.createMode": "Guarda primero la entrada para añadir etiquetas.",
|
|
62
|
+
"tags.section.loading": "Cargando…",
|
|
63
|
+
"tags.section.empty": "No se encontraron etiquetas.",
|
|
64
|
+
"tags.section.working": "Guardando…",
|
|
65
|
+
"tags.section.none": "Sin etiquetas",
|
|
66
|
+
"tags.section.edit": "Editar etiquetas",
|
|
67
|
+
"tags.manage.newLabel": "Nueva etiqueta",
|
|
68
|
+
"tags.manage.namePlaceholder": "Nombre de la etiqueta",
|
|
69
|
+
"tags.manage.scopeLabel": "Ámbito (tipo de entidad, vacío = global)",
|
|
70
|
+
"tags.manage.scopePlaceholder": "p. ej. nota (vacío = en todas partes)",
|
|
71
|
+
"tags.manage.create": "Crear etiqueta",
|
|
72
|
+
"tags.manage.edit": "Editar",
|
|
73
|
+
"tags.manage.save": "Guardar",
|
|
74
|
+
"tags.manage.cancel": "Cancelar",
|
|
75
|
+
"tags.manage.delete": "Eliminar",
|
|
76
|
+
"tags.manage.toggle": "Alternar etiqueta",
|
|
77
|
+
"tags.manage.usage": "{count}×",
|
|
78
|
+
"tags.manage.deleteConfirmTitle": "¿Eliminar la etiqueta «{name}»?",
|
|
79
|
+
"tags.manage.deleteConfirmDesc": "Se quitará de {count} objetos. Esto no se puede deshacer.",
|
|
80
|
+
"tags.picker.title": "Etiquetas",
|
|
81
|
+
"tags.picker.done": "Listo",
|
|
82
|
+
"tags.filter.label": "Filtrar por etiqueta",
|
|
83
|
+
"tags.filter.clear": "Quitar filtro",
|
|
84
|
+
},
|
|
60
85
|
};
|
|
@@ -36,4 +36,18 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
36
36
|
"template-resolver.editor.readOnly":
|
|
37
37
|
"Read-only — TenantAdmin or SystemAdmin role required to make changes.",
|
|
38
38
|
},
|
|
39
|
+
es: {
|
|
40
|
+
"template-resolver.editor.titleLabel": "Título",
|
|
41
|
+
"template-resolver.editor.contentLabel": "Contenido",
|
|
42
|
+
"template-resolver.editor.save": "Guardar",
|
|
43
|
+
"template-resolver.editor.saving": "Guardando…",
|
|
44
|
+
"template-resolver.editor.created": "Creado.",
|
|
45
|
+
"template-resolver.editor.saved": "Guardado.",
|
|
46
|
+
"template-resolver.editor.saveFailed": "Error al guardar.",
|
|
47
|
+
"template-resolver.editor.networkError": "Error de red al guardar.",
|
|
48
|
+
"template-resolver.editor.loading": "Cargando versión actual…",
|
|
49
|
+
"template-resolver.editor.loadFailed": "No se pudo cargar el bloque",
|
|
50
|
+
"template-resolver.editor.readOnly":
|
|
51
|
+
"Solo lectura — se requiere el rol TenantAdmin o SystemAdmin para hacer cambios.",
|
|
52
|
+
},
|
|
39
53
|
};
|