@cosmicdrift/kumiko-bundled-features 0.206.0 → 0.208.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 +9 -8
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +1 -1
- package/src/admin-shell/i18n.ts +17 -20
- package/src/admin-shell/web/i18n.ts +6 -11
- package/src/audit/i18n.ts +25 -25
- package/src/audit/web/i18n.ts +5 -9
- package/src/auth-email-password/__tests__/email-templates.test.ts +4 -0
- package/src/auth-email-password/email-templates.ts +97 -149
- package/src/auth-email-password/i18n.ts +6 -136
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +7 -7
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +14 -14
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +23 -25
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +60 -68
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +24 -24
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +20 -20
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +9 -9
- package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +11 -11
- package/src/auth-email-password/web/__tests__/test-utils.tsx +2 -2
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +3 -3
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +3 -3
- package/src/auth-email-password/web/invite-accept-screen.tsx +3 -1
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +2 -4
- package/src/auth-mfa/i18n.ts +1 -2
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +15 -15
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +45 -49
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +10 -10
- package/src/auth-mfa/web/i18n.ts +0 -63
- package/src/cap-counter/i18n.ts +5 -6
- package/src/compliance-profiles/i18n.ts +3 -3
- package/src/compliance-profiles/web/i18n.ts +0 -10
- package/src/config/i18n.ts +5 -5
- package/src/config/web/i18n.ts +1 -10
- package/src/custom-fields/web/i18n.ts +1 -9
- package/src/delivery/i18n.ts +3 -3
- package/src/delivery/web/i18n.ts +0 -10
- package/src/feature-toggles/i18n.ts +3 -3
- package/src/feature-toggles/web/i18n.ts +0 -12
- package/src/folders/web/i18n.ts +1 -26
- package/src/jobs/i18n.ts +40 -46
- package/src/jobs/web/i18n.ts +5 -9
- package/src/managed-pages/i18n.ts +23 -27
- package/src/managed-pages/web/i18n.ts +6 -11
- package/src/notes-history/i18n.ts +1 -1
- package/src/notes-history/web/i18n.ts +1 -9
- package/src/personal-access-tokens/i18n.ts +2 -2
- package/src/personal-access-tokens/web/i18n.ts +0 -37
- package/src/sessions/i18n.ts +17 -17
- package/src/tags/i18n.ts +2 -2
- package/src/tags/web/i18n.ts +1 -27
- package/src/template-resolver/web/__tests__/client-plugin.test.tsx +2 -2
- package/src/template-resolver/web/__tests__/editor-read-only.test.tsx +5 -5
- package/src/template-resolver/web/i18n.ts +0 -14
- package/src/tenant/__tests__/members-screens.boot.test.ts +0 -2
- package/src/tenant/i18n.ts +13 -16
- package/src/tenant/web/i18n.ts +0 -19
- package/src/tier-engine/i18n.ts +3 -19
- package/src/user/i18n.ts +14 -15
- package/src/user-data-rights/__tests__/default-mailers.test.ts +4 -0
- package/src/user-data-rights/__tests__/email-templates.test.ts +4 -0
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +4 -0
- package/src/user-data-rights/email-templates.ts +55 -91
- package/src/user-data-rights/i18n.ts +18 -22
- package/src/user-data-rights/lib/default-mailers.ts +4 -1
- package/src/user-data-rights/web/__tests__/deletion-screens.test.tsx +11 -11
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +5 -5
- package/src/user-data-rights/web/i18n.ts +9 -88
- package/src/user-profile/i18n.ts +1 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.208.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
127
127
|
},
|
|
128
128
|
"dependencies": {
|
|
129
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
130
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
131
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
132
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
133
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
129
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.208.0",
|
|
130
|
+
"@cosmicdrift/kumiko-framework": "0.208.0",
|
|
131
|
+
"@cosmicdrift/kumiko-headless": "0.208.0",
|
|
132
|
+
"@cosmicdrift/kumiko-renderer": "0.208.0",
|
|
133
|
+
"@cosmicdrift/kumiko-renderer-web": "0.208.0",
|
|
134
|
+
"@cosmicdrift/kumiko-types": "0.208.0",
|
|
135
135
|
"@mollie/api-client": "^4.5.0",
|
|
136
136
|
"@node-rs/argon2": "^2.0.2",
|
|
137
137
|
"@types/mailparser": "^3.4.6",
|
|
@@ -159,6 +159,7 @@
|
|
|
159
159
|
],
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@testing-library/user-event": "^14.6.1",
|
|
162
|
-
"@types/qrcode": "^1.5.5"
|
|
162
|
+
"@types/qrcode": "^1.5.5",
|
|
163
|
+
"@cosmicdrift/kumiko-locale-de": "0.208.0"
|
|
163
164
|
}
|
|
164
165
|
}
|
|
@@ -141,7 +141,7 @@ describe("admin-shell boot + workspace composition", () => {
|
|
|
141
141
|
|
|
142
142
|
test("admin-shell registers server translations bundle", () => {
|
|
143
143
|
const shell = createAdminShellFeature();
|
|
144
|
-
expect(shell.translations?.["admin-shell:nav.tenantOverview"]?.["
|
|
144
|
+
expect(shell.translations?.["admin-shell:nav.tenantOverview"]?.["en"]).toBe("Overview");
|
|
145
145
|
expect(shell.translations?.["screen:tenant-overview.title"]?.["en"]).toBe("Overview");
|
|
146
146
|
});
|
|
147
147
|
});
|
package/src/admin-shell/i18n.ts
CHANGED
|
@@ -2,35 +2,32 @@
|
|
|
2
2
|
// Pure-Data i18n keys for admin-shell (server r.translations + client pivot).
|
|
3
3
|
// Without these keys nav labels and workspace tabs render raw QNs in the shell.
|
|
4
4
|
|
|
5
|
-
type LocalizedString = { readonly
|
|
5
|
+
type LocalizedString = { readonly en: string };
|
|
6
6
|
|
|
7
7
|
export const ADMIN_SHELL_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
8
|
-
"screen:tenant-overview.title": {
|
|
9
|
-
"screen:platform-overview.title": {
|
|
10
|
-
"admin-shell:workspace.tenant": {
|
|
11
|
-
"admin-shell:workspace.platform": {
|
|
12
|
-
"admin-shell:nav.tenantOverview": {
|
|
13
|
-
"admin-shell:nav.platformOverview": {
|
|
14
|
-
"admin-shell:nav.tenants": {
|
|
15
|
-
"admin-shell:nav.tierAdmin": {
|
|
16
|
-
"admin-shell:overview.tenantTitle": {
|
|
17
|
-
"admin-shell:overview.platformTitle": {
|
|
18
|
-
"admin-shell:overview.loading": {
|
|
8
|
+
"screen:tenant-overview.title": { en: "Overview" },
|
|
9
|
+
"screen:platform-overview.title": { en: "Overview" },
|
|
10
|
+
"admin-shell:workspace.tenant": { en: "Administration" },
|
|
11
|
+
"admin-shell:workspace.platform": { en: "Platform" },
|
|
12
|
+
"admin-shell:nav.tenantOverview": { en: "Overview" },
|
|
13
|
+
"admin-shell:nav.platformOverview": { en: "Overview" },
|
|
14
|
+
"admin-shell:nav.tenants": { en: "Tenants" },
|
|
15
|
+
"admin-shell:nav.tierAdmin": { en: "Assign tier" },
|
|
16
|
+
"admin-shell:overview.tenantTitle": { en: "Administration" },
|
|
17
|
+
"admin-shell:overview.platformTitle": { en: "Platform" },
|
|
18
|
+
"admin-shell:overview.loading": { en: "Loading…" },
|
|
19
19
|
"admin-shell:overview.pendingInvitations": {
|
|
20
|
-
de: "Ausstehende Einladungen",
|
|
21
20
|
en: "Pending invitations",
|
|
22
21
|
},
|
|
23
|
-
"admin-shell:overview.members": {
|
|
22
|
+
"admin-shell:overview.members": { en: "Members" },
|
|
24
23
|
"admin-shell:overview.missingConfig": {
|
|
25
|
-
de: "Fehlende Konfiguration",
|
|
26
24
|
en: "Missing configuration",
|
|
27
25
|
},
|
|
28
26
|
"admin-shell:overview.missingConfigHint": {
|
|
29
|
-
de: "Pflichtfelder in den Einstellungen prüfen",
|
|
30
27
|
en: "Check required settings",
|
|
31
28
|
},
|
|
32
|
-
"admin-shell:overview.tenants": {
|
|
33
|
-
"admin-shell:overview.users": {
|
|
34
|
-
"admin-shell:overview.failedJobs": {
|
|
35
|
-
"admin-shell:overview.failedJobsHint": {
|
|
29
|
+
"admin-shell:overview.tenants": { en: "Tenants" },
|
|
30
|
+
"admin-shell:overview.users": { en: "Users" },
|
|
31
|
+
"admin-shell:overview.failedJobs": { en: "Failed jobs" },
|
|
32
|
+
"admin-shell:overview.failedJobsHint": { en: "Review job runs" },
|
|
36
33
|
};
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Client pivot of ADMIN_SHELL_I18N — same keys as server r.translations bundle.
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
type TranslationsByLocale,
|
|
6
|
+
translationsByLocaleFromKeys,
|
|
7
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
5
8
|
import { ADMIN_SHELL_I18N } from "../i18n";
|
|
6
9
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
export const defaultTranslations: TranslationsByLocale = Object.fromEntries(
|
|
10
|
-
LOCALES.map((locale) => [
|
|
11
|
-
locale,
|
|
12
|
-
Object.fromEntries(
|
|
13
|
-
Object.entries(ADMIN_SHELL_I18N).map(([key, value]) => [key, value[locale]]),
|
|
14
|
-
),
|
|
15
|
-
]),
|
|
16
|
-
);
|
|
10
|
+
export const defaultTranslations: TranslationsByLocale =
|
|
11
|
+
translationsByLocaleFromKeys(ADMIN_SHELL_I18N);
|
package/src/audit/i18n.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Server + client i18n for audit (nav labels + AuditLogScreen).
|
|
3
3
|
|
|
4
|
-
type LocalizedString = { readonly
|
|
4
|
+
type LocalizedString = { readonly en: string };
|
|
5
5
|
|
|
6
6
|
export const AUDIT_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
-
"screen:audit-log.title": {
|
|
8
|
-
"screen:audit-log-detail.title": {
|
|
9
|
-
"audit:nav.auditLog": {
|
|
10
|
-
"audit.log.title": {
|
|
11
|
-
"audit.log.loading": {
|
|
12
|
-
"audit.log.empty": {
|
|
13
|
-
"audit.log.newest": {
|
|
14
|
-
"audit.log.older": {
|
|
15
|
-
"audit.log.col.when": {
|
|
16
|
-
"audit.log.col.type": {
|
|
17
|
-
"audit.log.col.aggregate": {
|
|
18
|
-
"audit.log.col.actor": {
|
|
19
|
-
"audit.log.filter.eventType": {
|
|
20
|
-
"audit.log.filter.aggregateType": {
|
|
21
|
-
"audit.log.filter.from": {
|
|
22
|
-
"audit.log.filter.to": {
|
|
23
|
-
"audit.log.filter.apply": {
|
|
24
|
-
"audit.log.filter.reset": {
|
|
25
|
-
"audit.log.details": {
|
|
26
|
-
"audit.log.detail.loading": {
|
|
27
|
-
"audit.log.detail.missing": {
|
|
28
|
-
"audit.log.detail.payload": {
|
|
29
|
-
"audit.log.detail.metadata": {
|
|
30
|
-
"audit.log.detail.field.id": {
|
|
7
|
+
"screen:audit-log.title": { en: "Audit log" },
|
|
8
|
+
"screen:audit-log-detail.title": { en: "Event" },
|
|
9
|
+
"audit:nav.auditLog": { en: "Audit" },
|
|
10
|
+
"audit.log.title": { en: "Audit log" },
|
|
11
|
+
"audit.log.loading": { en: "Loading events…" },
|
|
12
|
+
"audit.log.empty": { en: "No events." },
|
|
13
|
+
"audit.log.newest": { en: "Newest" },
|
|
14
|
+
"audit.log.older": { en: "Load older" },
|
|
15
|
+
"audit.log.col.when": { en: "When" },
|
|
16
|
+
"audit.log.col.type": { en: "Event" },
|
|
17
|
+
"audit.log.col.aggregate": { en: "Aggregate" },
|
|
18
|
+
"audit.log.col.actor": { en: "Actor" },
|
|
19
|
+
"audit.log.filter.eventType": { en: "Event type" },
|
|
20
|
+
"audit.log.filter.aggregateType": { en: "Aggregate type" },
|
|
21
|
+
"audit.log.filter.from": { en: "From" },
|
|
22
|
+
"audit.log.filter.to": { en: "To" },
|
|
23
|
+
"audit.log.filter.apply": { en: "Filter" },
|
|
24
|
+
"audit.log.filter.reset": { en: "Reset" },
|
|
25
|
+
"audit.log.details": { en: "Details" },
|
|
26
|
+
"audit.log.detail.loading": { en: "Loading event…" },
|
|
27
|
+
"audit.log.detail.missing": { en: "Event not found." },
|
|
28
|
+
"audit.log.detail.payload": { en: "Event payload" },
|
|
29
|
+
"audit.log.detail.metadata": { en: "Metadata" },
|
|
30
|
+
"audit.log.detail.field.id": { en: "Event ID" },
|
|
31
31
|
};
|
package/src/audit/web/i18n.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type TranslationsByLocale,
|
|
4
|
+
translationsByLocaleFromKeys,
|
|
5
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
3
6
|
import { AUDIT_I18N } from "../i18n";
|
|
4
7
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
export const defaultTranslations: TranslationsByLocale = Object.fromEntries(
|
|
8
|
-
LOCALES.map((locale) => [
|
|
9
|
-
locale,
|
|
10
|
-
Object.fromEntries(Object.entries(AUDIT_I18N).map(([key, value]) => [key, value[locale]])),
|
|
11
|
-
]),
|
|
12
|
-
);
|
|
8
|
+
export const defaultTranslations: TranslationsByLocale = translationsByLocaleFromKeys(AUDIT_I18N);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// dort. Diese Tests prüfen daher die strukturierte Content, nicht HTML.
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
8
|
+
import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
|
|
7
9
|
import type { AuthMailContent } from "../email-templates";
|
|
8
10
|
import { renderResetPasswordEmail, renderVerifyEmail } from "../email-templates";
|
|
9
11
|
|
|
@@ -18,6 +20,8 @@ function textOf(content: AuthMailContent): string {
|
|
|
18
20
|
return content.sections.map((section) => ("text" in section ? section.text : "")).join(" ");
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
registerMailTranslations("de", localeDeBundle);
|
|
24
|
+
|
|
21
25
|
describe("renderResetPasswordEmail", () => {
|
|
22
26
|
const baseArgs = {
|
|
23
27
|
url: "https://acme.example/reset?token=t-abc123",
|
|
@@ -5,33 +5,71 @@
|
|
|
5
5
|
// renderer; these templates are deliberately plain so the renderer (and the
|
|
6
6
|
// operator reading the mailer log) can rely on them.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// English ships here. Other languages register via @cosmicdrift/kumiko-locale-*
|
|
9
|
+
// (localeDe() / localeEs() call registerMailTranslations).
|
|
9
10
|
|
|
11
|
+
import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
10
12
|
import { Temporal } from "temporal-polyfill";
|
|
11
13
|
|
|
12
|
-
export type AuthMailLocale =
|
|
14
|
+
export type AuthMailLocale = string;
|
|
15
|
+
|
|
16
|
+
const AUTH_MAIL_EN: Readonly<Record<string, string>> = {
|
|
17
|
+
"auth.mail.appNameDefault": "Account",
|
|
18
|
+
"auth.mail.reset.subject": "{app} — Reset your password",
|
|
19
|
+
"auth.mail.reset.greeting": "Hi,",
|
|
20
|
+
"auth.mail.reset.intro":
|
|
21
|
+
"you requested a password reset for {app}. Click the link below to set a new password:",
|
|
22
|
+
"auth.mail.reset.button": "Reset password",
|
|
23
|
+
"auth.mail.reset.expiry": "The link expires on {when}.",
|
|
24
|
+
"auth.mail.reset.ignore":
|
|
25
|
+
"If you didn't request a reset, you can safely ignore this email — your password won't change.",
|
|
26
|
+
"auth.mail.verify.subject": "{app} — Verify your email",
|
|
27
|
+
"auth.mail.verify.greeting": "Welcome,",
|
|
28
|
+
"auth.mail.verify.intro": "please verify your email address for {app} to activate your account:",
|
|
29
|
+
"auth.mail.verify.button": "Verify email",
|
|
30
|
+
"auth.mail.verify.expiry": "The link expires on {when}.",
|
|
31
|
+
"auth.mail.verify.ignore": "If you didn't create this account, you can ignore this email.",
|
|
32
|
+
"auth.mail.activation.subject": "{app} — Activate your account",
|
|
33
|
+
"auth.mail.activation.greeting": "Welcome,",
|
|
34
|
+
"auth.mail.activation.intro":
|
|
35
|
+
"click the link below to activate your {app} account. The next step is choosing your password:",
|
|
36
|
+
"auth.mail.activation.button": "Activate account",
|
|
37
|
+
"auth.mail.activation.expiry": "The link expires on {when}.",
|
|
38
|
+
"auth.mail.activation.ignore":
|
|
39
|
+
"If you didn't sign up, you can ignore this email — no account is created until you open the link.",
|
|
40
|
+
"auth.mail.invite.subject": "{app} — Workspace invitation",
|
|
41
|
+
"auth.mail.invite.greeting": "Hi,",
|
|
42
|
+
"auth.mail.invite.intro":
|
|
43
|
+
"you've been invited to a {app} workspace as {role}. Click the link below to accept:",
|
|
44
|
+
"auth.mail.invite.button": "Accept invitation",
|
|
45
|
+
"auth.mail.invite.expiry": "The link expires on {when}.",
|
|
46
|
+
"auth.mail.invite.ignore": "If you weren't expecting this invitation, you can ignore this email.",
|
|
47
|
+
"auth.mail.unlock.subject": "{app} — Unlock your account",
|
|
48
|
+
"auth.mail.unlock.greeting": "Hi,",
|
|
49
|
+
"auth.mail.unlock.intro":
|
|
50
|
+
"your {app} account was temporarily locked after several failed sign-in attempts. Click the link below to unlock it immediately:",
|
|
51
|
+
"auth.mail.unlock.button": "Unlock account",
|
|
52
|
+
"auth.mail.unlock.expiry": "The link expires on {when}.",
|
|
53
|
+
"auth.mail.unlock.ignore":
|
|
54
|
+
"If you didn't trigger this lock, you can ignore this email — the lock expires on its own.",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
registerMailTranslations("en", AUTH_MAIL_EN);
|
|
13
58
|
|
|
14
|
-
// Unified args for the structured token-mail renderers (reset + verify).
|
|
15
|
-
// `url` is the fully-built magic-link — the handler already appended ?token=.
|
|
16
59
|
export type RenderTokenContentArgs = {
|
|
17
60
|
readonly url: string;
|
|
18
61
|
readonly expiresAt: string;
|
|
19
62
|
readonly locale?: AuthMailLocale;
|
|
20
|
-
/** Optional
|
|
63
|
+
/** Optional app name for subject + intro. Default from auth.mail.appNameDefault. */
|
|
21
64
|
readonly appName?: string;
|
|
22
65
|
};
|
|
23
66
|
|
|
24
|
-
// Invite adds the role to the unified token-content args; `url` is the fully-
|
|
25
|
-
// built magic-link (the handler already appended ?token=).
|
|
26
67
|
export type RenderInviteEmailArgs = RenderTokenContentArgs & { readonly role: string };
|
|
27
68
|
|
|
28
69
|
export type AuthMailSection =
|
|
29
70
|
| { readonly text: string }
|
|
30
71
|
| { readonly button: { readonly label: string; readonly url: string } };
|
|
31
72
|
|
|
32
|
-
// Structured content the magic-link handlers pass as the delivery `data`
|
|
33
|
-
// payload: renderer-simple turns header/sections/footer into HTML, the
|
|
34
|
-
// email-channel reads `subject`. No pre-rendered HTML — the renderer owns layout.
|
|
35
73
|
export type AuthMailContent = {
|
|
36
74
|
readonly subject: string;
|
|
37
75
|
readonly header: string;
|
|
@@ -39,95 +77,6 @@ export type AuthMailContent = {
|
|
|
39
77
|
readonly footer: string;
|
|
40
78
|
};
|
|
41
79
|
|
|
42
|
-
const STRINGS = {
|
|
43
|
-
de: {
|
|
44
|
-
resetSubject: (app: string) => `${app} — Passwort zurücksetzen`,
|
|
45
|
-
resetGreeting: "Hallo,",
|
|
46
|
-
// guard:dup-ok — false positive: i18n-String-Template, gleiche Arrow-Struktur wie anonymous fn in collect-table-metas
|
|
47
|
-
resetIntro: (app: string) =>
|
|
48
|
-
`du hast den Reset deines Passworts für ${app} angefordert. Klicke auf den folgenden Link, um ein neues Passwort zu setzen:`,
|
|
49
|
-
resetButton: "Passwort zurücksetzen",
|
|
50
|
-
resetExpiry: (when: string) => `Der Link läuft am ${when} ab.`,
|
|
51
|
-
resetIgnore:
|
|
52
|
-
"Falls du keinen Reset angefordert hast, kannst du diese E-Mail einfach ignorieren — dein Passwort bleibt unverändert.",
|
|
53
|
-
verifySubject: (app: string) => `${app} — E-Mail bestätigen`,
|
|
54
|
-
verifyGreeting: "Willkommen,",
|
|
55
|
-
verifyIntro: (app: string) =>
|
|
56
|
-
`bitte bestätige deine E-Mail-Adresse für ${app}, um dein Konto zu aktivieren:`,
|
|
57
|
-
verifyButton: "E-Mail bestätigen",
|
|
58
|
-
verifyExpiry: (when: string) => `Der Link läuft am ${when} ab.`,
|
|
59
|
-
verifyIgnore: "Falls du dieses Konto nicht angelegt hast, kannst du diese E-Mail ignorieren.",
|
|
60
|
-
activationSubject: (app: string) => `${app} — Account aktivieren`,
|
|
61
|
-
activationGreeting: "Willkommen,",
|
|
62
|
-
activationIntro: (app: string) =>
|
|
63
|
-
`klicke auf den folgenden Link, um deinen ${app}-Account zu aktivieren. Im nächsten Schritt setzt du dein Passwort:`,
|
|
64
|
-
activationButton: "Account aktivieren",
|
|
65
|
-
activationExpiry: (when: string) => `Der Link läuft am ${when} ab.`,
|
|
66
|
-
activationIgnore:
|
|
67
|
-
"Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren — es wird kein Account erstellt, solange du den Link nicht öffnest.",
|
|
68
|
-
inviteSubject: (app: string) => `${app} — Einladung zum Workspace`,
|
|
69
|
-
inviteGreeting: "Hallo,",
|
|
70
|
-
inviteIntro: (app: string, role: string) =>
|
|
71
|
-
`du wurdest zu einem ${app}-Workspace als ${role} eingeladen. Klicke auf den folgenden Link, um die Einladung anzunehmen:`,
|
|
72
|
-
inviteButton: "Einladung annehmen",
|
|
73
|
-
inviteExpiry: (when: string) => `Der Link läuft am ${when} ab.`,
|
|
74
|
-
inviteIgnore:
|
|
75
|
-
"Falls du diese Einladung nicht erwartet hast, kannst du diese E-Mail ignorieren.",
|
|
76
|
-
unlockSubject: (app: string) => `${app} — Konto entsperren`,
|
|
77
|
-
unlockGreeting: "Hallo,",
|
|
78
|
-
unlockIntro: (app: string) =>
|
|
79
|
-
`dein ${app}-Konto wurde nach mehreren fehlgeschlagenen Anmeldeversuchen vorübergehend gesperrt. Klicke auf den folgenden Link, um es sofort zu entsperren:`,
|
|
80
|
-
unlockButton: "Konto entsperren",
|
|
81
|
-
unlockExpiry: (when: string) => `Der Link läuft am ${when} ab.`,
|
|
82
|
-
unlockIgnore:
|
|
83
|
-
"Falls du diese Sperre nicht ausgelöst hast, kannst du diese E-Mail ignorieren — die Sperre läuft von selbst ab.",
|
|
84
|
-
},
|
|
85
|
-
en: {
|
|
86
|
-
resetSubject: (app: string) => `${app} — Reset your password`,
|
|
87
|
-
resetGreeting: "Hi,",
|
|
88
|
-
resetIntro: (app: string) =>
|
|
89
|
-
`you requested a password reset for ${app}. Click the link below to set a new password:`,
|
|
90
|
-
resetButton: "Reset password",
|
|
91
|
-
resetExpiry: (when: string) => `The link expires on ${when}.`,
|
|
92
|
-
resetIgnore:
|
|
93
|
-
"If you didn't request a reset, you can safely ignore this email — your password won't change.",
|
|
94
|
-
verifySubject: (app: string) => `${app} — Verify your email`,
|
|
95
|
-
verifyGreeting: "Welcome,",
|
|
96
|
-
verifyIntro: (app: string) =>
|
|
97
|
-
`please verify your email address for ${app} to activate your account:`,
|
|
98
|
-
verifyButton: "Verify email",
|
|
99
|
-
verifyExpiry: (when: string) => `The link expires on ${when}.`,
|
|
100
|
-
verifyIgnore: "If you didn't create this account, you can ignore this email.",
|
|
101
|
-
activationSubject: (app: string) => `${app} — Activate your account`,
|
|
102
|
-
activationGreeting: "Welcome,",
|
|
103
|
-
activationIntro: (app: string) =>
|
|
104
|
-
`click the link below to activate your ${app} account. The next step is choosing your password:`,
|
|
105
|
-
activationButton: "Activate account",
|
|
106
|
-
activationExpiry: (when: string) => `The link expires on ${when}.`,
|
|
107
|
-
activationIgnore:
|
|
108
|
-
"If you didn't sign up, you can ignore this email — no account is created until you open the link.",
|
|
109
|
-
inviteSubject: (app: string) => `${app} — Workspace invitation`,
|
|
110
|
-
inviteGreeting: "Hi,",
|
|
111
|
-
inviteIntro: (app: string, role: string) =>
|
|
112
|
-
`you've been invited to a ${app} workspace as ${role}. Click the link below to accept:`,
|
|
113
|
-
inviteButton: "Accept invitation",
|
|
114
|
-
inviteExpiry: (when: string) => `The link expires on ${when}.`,
|
|
115
|
-
inviteIgnore: "If you weren't expecting this invitation, you can ignore this email.",
|
|
116
|
-
unlockSubject: (app: string) => `${app} — Unlock your account`,
|
|
117
|
-
unlockGreeting: "Hi,",
|
|
118
|
-
unlockIntro: (app: string) =>
|
|
119
|
-
`your ${app} account was temporarily locked after several failed sign-in attempts. Click the link below to unlock it immediately:`,
|
|
120
|
-
unlockButton: "Unlock account",
|
|
121
|
-
unlockExpiry: (when: string) => `The link expires on ${when}.`,
|
|
122
|
-
unlockIgnore:
|
|
123
|
-
"If you didn't trigger this lock, you can ignore this email — the lock expires on its own.",
|
|
124
|
-
},
|
|
125
|
-
} as const;
|
|
126
|
-
|
|
127
|
-
// Structured content for the delivery path. header = action title (CTA label),
|
|
128
|
-
// sections = greeting/intro/button/expiry, footer = the "ignore if not you"
|
|
129
|
-
// reassurance. The old plain-text fallback-URL link drops out — renderer-simple
|
|
130
|
-
// has no link-text section and the button carries the URL.
|
|
131
80
|
function tokenMailContent(spec: {
|
|
132
81
|
readonly subject: string;
|
|
133
82
|
readonly header: string;
|
|
@@ -151,91 +100,90 @@ function tokenMailContent(spec: {
|
|
|
151
100
|
};
|
|
152
101
|
}
|
|
153
102
|
|
|
103
|
+
function t(locale: string, key: string, params?: Readonly<Record<string, string>>): string {
|
|
104
|
+
return mailT(locale, key, params);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function appNameFor(args: RenderTokenContentArgs): string {
|
|
108
|
+
const locale = args.locale ?? "en";
|
|
109
|
+
return args.appName ?? t(locale, "auth.mail.appNameDefault");
|
|
110
|
+
}
|
|
111
|
+
|
|
154
112
|
export function renderResetPasswordEmail(args: RenderTokenContentArgs): AuthMailContent {
|
|
155
113
|
const locale = args.locale ?? "en";
|
|
156
|
-
const
|
|
157
|
-
const t = STRINGS[locale];
|
|
114
|
+
const app = appNameFor(args);
|
|
158
115
|
return tokenMailContent({
|
|
159
|
-
subject: t.
|
|
160
|
-
header: t.
|
|
161
|
-
greeting: t.
|
|
162
|
-
intro: t.
|
|
163
|
-
buttonLabel: t.
|
|
116
|
+
subject: t(locale, "auth.mail.reset.subject", { app }),
|
|
117
|
+
header: t(locale, "auth.mail.reset.button"),
|
|
118
|
+
greeting: t(locale, "auth.mail.reset.greeting"),
|
|
119
|
+
intro: t(locale, "auth.mail.reset.intro", { app }),
|
|
120
|
+
buttonLabel: t(locale, "auth.mail.reset.button"),
|
|
164
121
|
buttonUrl: args.url,
|
|
165
|
-
expiry: t.
|
|
166
|
-
ignore: t.
|
|
122
|
+
expiry: t(locale, "auth.mail.reset.expiry", { when: formatExpiry(args.expiresAt) }),
|
|
123
|
+
ignore: t(locale, "auth.mail.reset.ignore"),
|
|
167
124
|
});
|
|
168
125
|
}
|
|
169
126
|
|
|
170
127
|
export function renderUnlockAccountEmail(args: RenderTokenContentArgs): AuthMailContent {
|
|
171
128
|
const locale = args.locale ?? "en";
|
|
172
|
-
const
|
|
173
|
-
const t = STRINGS[locale];
|
|
129
|
+
const app = appNameFor(args);
|
|
174
130
|
return tokenMailContent({
|
|
175
|
-
subject: t.
|
|
176
|
-
header: t.
|
|
177
|
-
greeting: t.
|
|
178
|
-
intro: t.
|
|
179
|
-
buttonLabel: t.
|
|
131
|
+
subject: t(locale, "auth.mail.unlock.subject", { app }),
|
|
132
|
+
header: t(locale, "auth.mail.unlock.button"),
|
|
133
|
+
greeting: t(locale, "auth.mail.unlock.greeting"),
|
|
134
|
+
intro: t(locale, "auth.mail.unlock.intro", { app }),
|
|
135
|
+
buttonLabel: t(locale, "auth.mail.unlock.button"),
|
|
180
136
|
buttonUrl: args.url,
|
|
181
|
-
expiry: t.
|
|
182
|
-
ignore: t.
|
|
137
|
+
expiry: t(locale, "auth.mail.unlock.expiry", { when: formatExpiry(args.expiresAt) }),
|
|
138
|
+
ignore: t(locale, "auth.mail.unlock.ignore"),
|
|
183
139
|
});
|
|
184
140
|
}
|
|
185
141
|
|
|
186
142
|
export function renderVerifyEmail(args: RenderTokenContentArgs): AuthMailContent {
|
|
187
143
|
const locale = args.locale ?? "en";
|
|
188
|
-
const
|
|
189
|
-
const t = STRINGS[locale];
|
|
144
|
+
const app = appNameFor(args);
|
|
190
145
|
return tokenMailContent({
|
|
191
|
-
subject: t.
|
|
192
|
-
header: t.
|
|
193
|
-
greeting: t.
|
|
194
|
-
intro: t.
|
|
195
|
-
buttonLabel: t.
|
|
146
|
+
subject: t(locale, "auth.mail.verify.subject", { app }),
|
|
147
|
+
header: t(locale, "auth.mail.verify.button"),
|
|
148
|
+
greeting: t(locale, "auth.mail.verify.greeting"),
|
|
149
|
+
intro: t(locale, "auth.mail.verify.intro", { app }),
|
|
150
|
+
buttonLabel: t(locale, "auth.mail.verify.button"),
|
|
196
151
|
buttonUrl: args.url,
|
|
197
|
-
expiry: t.
|
|
198
|
-
ignore: t.
|
|
152
|
+
expiry: t(locale, "auth.mail.verify.expiry", { when: formatExpiry(args.expiresAt) }),
|
|
153
|
+
ignore: t(locale, "auth.mail.verify.ignore"),
|
|
199
154
|
});
|
|
200
155
|
}
|
|
201
156
|
|
|
202
157
|
export function renderActivationEmail(args: RenderTokenContentArgs): AuthMailContent {
|
|
203
158
|
const locale = args.locale ?? "en";
|
|
204
|
-
const
|
|
205
|
-
const t = STRINGS[locale];
|
|
159
|
+
const app = appNameFor(args);
|
|
206
160
|
return tokenMailContent({
|
|
207
|
-
subject: t.
|
|
208
|
-
header: t.
|
|
209
|
-
greeting: t.
|
|
210
|
-
intro: t.
|
|
211
|
-
buttonLabel: t.
|
|
161
|
+
subject: t(locale, "auth.mail.activation.subject", { app }),
|
|
162
|
+
header: t(locale, "auth.mail.activation.button"),
|
|
163
|
+
greeting: t(locale, "auth.mail.activation.greeting"),
|
|
164
|
+
intro: t(locale, "auth.mail.activation.intro", { app }),
|
|
165
|
+
buttonLabel: t(locale, "auth.mail.activation.button"),
|
|
212
166
|
buttonUrl: args.url,
|
|
213
|
-
expiry: t.
|
|
214
|
-
ignore: t.
|
|
167
|
+
expiry: t(locale, "auth.mail.activation.expiry", { when: formatExpiry(args.expiresAt) }),
|
|
168
|
+
ignore: t(locale, "auth.mail.activation.ignore"),
|
|
215
169
|
});
|
|
216
170
|
}
|
|
217
171
|
|
|
218
172
|
export function renderInviteEmail(args: RenderInviteEmailArgs): AuthMailContent {
|
|
219
173
|
const locale = args.locale ?? "en";
|
|
220
|
-
const
|
|
221
|
-
const t = STRINGS[locale];
|
|
174
|
+
const app = args.appName ?? "Workspace";
|
|
222
175
|
return tokenMailContent({
|
|
223
|
-
subject: t.
|
|
224
|
-
header: t.
|
|
225
|
-
greeting: t.
|
|
226
|
-
intro: t
|
|
227
|
-
buttonLabel: t.
|
|
176
|
+
subject: t(locale, "auth.mail.invite.subject", { app }),
|
|
177
|
+
header: t(locale, "auth.mail.invite.button"),
|
|
178
|
+
greeting: t(locale, "auth.mail.invite.greeting"),
|
|
179
|
+
intro: t(locale, "auth.mail.invite.intro", { app, role: args.role }),
|
|
180
|
+
buttonLabel: t(locale, "auth.mail.invite.button"),
|
|
228
181
|
buttonUrl: args.url,
|
|
229
|
-
expiry: t.
|
|
230
|
-
ignore: t.
|
|
182
|
+
expiry: t(locale, "auth.mail.invite.expiry", { when: formatExpiry(args.expiresAt) }),
|
|
183
|
+
ignore: t(locale, "auth.mail.invite.ignore"),
|
|
231
184
|
});
|
|
232
185
|
}
|
|
233
186
|
|
|
234
|
-
// ISO-Timestamp aus dem Token-Handler ("2026-05-04T13:45:00.000Z") in
|
|
235
|
-
// "2026-05-04 13:45 UTC" rendern. Locale-unabhängig damit der Mail-
|
|
236
|
-
// Renderer keine locale-spezifischen Number-Formatter mitschleppt; UTC-
|
|
237
|
-
// Suffix damit der User unabhängig von seiner Tz sieht wann der Link
|
|
238
|
-
// abläuft. Bei un-parsbarem Input fällt's auf den raw-string zurück.
|
|
239
187
|
function formatExpiry(iso: string): string {
|
|
240
188
|
try {
|
|
241
189
|
const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
|