feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,309 @@
|
||||
import type { Locale } from "@/i18n/config";
|
||||
|
||||
/**
|
||||
* Eigenstaendiges Uebersetzungs-Woerterbuch nur fuer Transaktions-E-Mails —
|
||||
* bewusst getrennt vom UI-Dictionary (i18n/dictionary-type.ts), da E-Mails
|
||||
* eigene, laengere Fliesstexte haben und unabhaengig vom UI-Kontext (z.B.
|
||||
* beim Stripe-Webhook ohne Request/Cookie-Zugriff) gerendert werden.
|
||||
*/
|
||||
interface EmailStrings {
|
||||
orderConfirmation: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
subtotal: string;
|
||||
shipping: string;
|
||||
freeShipping: string;
|
||||
discount: string;
|
||||
total: string;
|
||||
deliveryTo: string;
|
||||
pickupNote: string;
|
||||
giftNote: string;
|
||||
trackOrder: string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
newsletterConfirm: {
|
||||
preheader: string;
|
||||
heading: string;
|
||||
intro: string;
|
||||
button: string;
|
||||
ignoreNote: string;
|
||||
subject: string;
|
||||
};
|
||||
passwordReset: {
|
||||
preheader: string;
|
||||
heading: string;
|
||||
intro: string;
|
||||
button: string;
|
||||
ignoreNote: string;
|
||||
subject: string;
|
||||
};
|
||||
securityAlert: {
|
||||
preheader: string;
|
||||
heading: string;
|
||||
contactNote: string;
|
||||
subject: string;
|
||||
passwordChanged: string;
|
||||
twoFactorDisabled: string;
|
||||
accountDeleted: string;
|
||||
};
|
||||
emailVerification: {
|
||||
preheader: string;
|
||||
heading: string;
|
||||
intro: string;
|
||||
button: string;
|
||||
ignoreNote: string;
|
||||
subject: string;
|
||||
};
|
||||
shipment: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
trackButton: string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
pickupReady: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
backInStock: {
|
||||
preheader: (name: string) => string;
|
||||
heading: string;
|
||||
intro: (name: string) => string;
|
||||
button: string;
|
||||
subject: (name: string) => string;
|
||||
};
|
||||
returnAccepted: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
returnRejected: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
returnRefunded: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
paymentFailed: {
|
||||
preheader: (orderNumber: string) => string;
|
||||
heading: (firstName: string) => string;
|
||||
intro: (orderNumber: string) => string;
|
||||
retryNote: string;
|
||||
subject: (orderNumber: string) => string;
|
||||
};
|
||||
}
|
||||
|
||||
const de: EmailStrings = {
|
||||
orderConfirmation: {
|
||||
preheader: (orderNumber) => `Deine Bestellung ${orderNumber} ist bestätigt.`,
|
||||
heading: (firstName) => `Danke für deine Bestellung, ${firstName}!`,
|
||||
intro: (orderNumber) => `Wir haben deine Bestellung <strong>${orderNumber}</strong> erhalten und die Zahlung bestätigt.`,
|
||||
subtotal: "Zwischensumme",
|
||||
shipping: "Versand",
|
||||
freeShipping: "kostenlos",
|
||||
discount: "Rabatt",
|
||||
total: "Gesamt",
|
||||
deliveryTo: "Lieferung an:",
|
||||
pickupNote: "Abholung in Altenmünster — wir melden uns, sobald deine Bestellung bereitsteht.",
|
||||
giftNote: "🎁 Deine persönliche Nachricht wird von Hand auf eine Karte geschrieben und der Bestellung beigelegt.",
|
||||
trackOrder: "BESTELLUNG VERFOLGEN",
|
||||
subject: (orderNumber) => `Bestellbestätigung ${orderNumber} — Wabenhain`,
|
||||
},
|
||||
newsletterConfirm: {
|
||||
preheader: "Bitte bestätige deine Anmeldung zur Erntepost.",
|
||||
heading: "Fast geschafft!",
|
||||
intro: "Bitte bestätige deine Anmeldung zur Erntepost, damit wir dich informieren dürfen, sobald neuer Honig verfügbar ist.",
|
||||
button: "ANMELDUNG BESTÄTIGEN",
|
||||
ignoreNote: "Falls du dich nicht angemeldet hast, kannst du diese E-Mail einfach ignorieren.",
|
||||
subject: "Bitte bestätige deine Anmeldung — Wabenhain Erntepost",
|
||||
},
|
||||
passwordReset: {
|
||||
preheader: "Setze dein Wabenhain-Passwort zurück.",
|
||||
heading: "Passwort zurücksetzen",
|
||||
intro: "Wir haben eine Anfrage erhalten, das Passwort für dein Wabenhain-Konto zurückzusetzen. Der Link ist eine Stunde gültig.",
|
||||
button: "NEUES PASSWORT VERGEBEN",
|
||||
ignoreNote: "Falls du das nicht warst, kannst du diese E-Mail ignorieren — dein Passwort bleibt unverändert.",
|
||||
subject: "Passwort zurücksetzen — Wabenhain",
|
||||
},
|
||||
securityAlert: {
|
||||
preheader: "Sicherheitshinweis zu deinem Wabenhain-Konto.",
|
||||
heading: "Sicherheitshinweis",
|
||||
contactNote: "Falls das nicht du warst, kontaktiere uns bitte umgehend unter [email protected].",
|
||||
subject: "Sicherheitshinweis — Wabenhain",
|
||||
passwordChanged: "Das Passwort für dein Wabenhain-Konto wurde soeben geändert. Alle anderen angemeldeten Geräte wurden abgemeldet.",
|
||||
twoFactorDisabled: "Die Zwei-Faktor-Authentifizierung für dein Wabenhain-Konto wurde soeben deaktiviert.",
|
||||
accountDeleted: "Dein Wabenhain-Konto und alle persönlichen Daten wurden soeben endgültig gelöscht.",
|
||||
},
|
||||
emailVerification: {
|
||||
preheader: "Bitte bestätige deine E-Mail-Adresse.",
|
||||
heading: "Willkommen bei Wabenhain!",
|
||||
intro: "Bitte bestätige deine E-Mail-Adresse, damit du mit deinem Konto bestellen kannst. Der Link ist 24 Stunden gültig.",
|
||||
button: "E-MAIL-ADRESSE BESTÄTIGEN",
|
||||
ignoreNote: "Falls du dieses Konto nicht erstellt hast, kannst du diese E-Mail einfach ignorieren.",
|
||||
subject: "Bitte bestätige deine E-Mail-Adresse — Wabenhain",
|
||||
},
|
||||
shipment: {
|
||||
preheader: (orderNumber) => `Deine Bestellung ${orderNumber} ist unterwegs.`,
|
||||
heading: (firstName) => `Deine Bestellung ist unterwegs, ${firstName}!`,
|
||||
intro: (orderNumber) => `Wir haben deine Bestellung <strong>${orderNumber}</strong> soeben versendet.`,
|
||||
trackButton: "SENDUNG VERFOLGEN",
|
||||
subject: (orderNumber) => `Deine Bestellung ${orderNumber} wurde versendet — Wabenhain`,
|
||||
},
|
||||
pickupReady: {
|
||||
preheader: (orderNumber) => `Deine Bestellung ${orderNumber} ist abholbereit.`,
|
||||
heading: (firstName) => `Deine Bestellung wartet auf dich, ${firstName}!`,
|
||||
intro: (orderNumber) => `Deine Bestellung <strong>${orderNumber}</strong> ist abholbereit.`,
|
||||
subject: (orderNumber) => `Deine Bestellung ${orderNumber} ist abholbereit — Wabenhain`,
|
||||
},
|
||||
backInStock: {
|
||||
preheader: (name) => `${name} ist wieder verfügbar.`,
|
||||
heading: "Gute Nachrichten!",
|
||||
intro: (name) => `<strong>${name}</strong> ist wieder auf Lager.`,
|
||||
button: "JETZT BESTELLEN",
|
||||
subject: (name) => `${name} ist wieder verfügbar — Wabenhain`,
|
||||
},
|
||||
returnAccepted: {
|
||||
preheader: (orderNumber) => `Deine Rücksendeanfrage zu ${orderNumber} wurde angenommen.`,
|
||||
heading: (firstName) => `Deine Rücksendung wurde angenommen, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`Wir haben deine Rücksendeanfrage zu Bestellung <strong>${orderNumber}</strong> angenommen. Bitte sende uns die Ware zurück — sobald sie bei uns eingegangen ist, veranlassen wir die Erstattung.`,
|
||||
subject: (orderNumber) => `Rücksendung zu ${orderNumber} angenommen — Wabenhain`,
|
||||
},
|
||||
returnRejected: {
|
||||
preheader: (orderNumber) => `Deine Rücksendeanfrage zu ${orderNumber} wurde abgelehnt.`,
|
||||
heading: (firstName) => `Deine Rücksendeanfrage wurde leider abgelehnt, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`Wir haben deine Rücksendeanfrage zu Bestellung <strong>${orderNumber}</strong> geprüft und können sie leider nicht annehmen. Bei Fragen dazu melde dich gerne bei uns.`,
|
||||
subject: (orderNumber) => `Rücksendung zu ${orderNumber} abgelehnt — Wabenhain`,
|
||||
},
|
||||
returnRefunded: {
|
||||
preheader: (orderNumber) => `Deine Bestellung ${orderNumber} wurde erstattet.`,
|
||||
heading: (firstName) => `Deine Erstattung ist unterwegs, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`Wir haben die Rückerstattung für deine Bestellung <strong>${orderNumber}</strong> veranlasst. Je nach Zahlungsmethode kann es einige Werktage dauern, bis der Betrag bei dir ankommt.`,
|
||||
subject: (orderNumber) => `Bestellung ${orderNumber} erstattet — Wabenhain`,
|
||||
},
|
||||
paymentFailed: {
|
||||
preheader: (orderNumber) => `Die Zahlung zu Bestellung ${orderNumber} konnte nicht abgeschlossen werden.`,
|
||||
heading: (firstName) => `Leider hat die Zahlung nicht geklappt, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`Der Zahlungseinzug für deine Bestellung <strong>${orderNumber}</strong> konnte nicht abgeschlossen werden. Wir haben die Bestellung daher storniert, es sind dir keine Kosten entstanden.`,
|
||||
retryNote: "Du kannst die Bestellung gerne erneut aufgeben, zum Beispiel mit einer anderen Zahlungsmethode.",
|
||||
subject: (orderNumber) => `Zahlung zu Bestellung ${orderNumber} fehlgeschlagen — Wabenhain`,
|
||||
},
|
||||
};
|
||||
|
||||
const en: EmailStrings = {
|
||||
orderConfirmation: {
|
||||
preheader: (orderNumber) => `Your order ${orderNumber} is confirmed.`,
|
||||
heading: (firstName) => `Thanks for your order, ${firstName}!`,
|
||||
intro: (orderNumber) => `We've received your order <strong>${orderNumber}</strong> and confirmed payment.`,
|
||||
subtotal: "Subtotal",
|
||||
shipping: "Shipping",
|
||||
freeShipping: "free",
|
||||
discount: "Discount",
|
||||
total: "Total",
|
||||
deliveryTo: "Delivering to:",
|
||||
pickupNote: "Pickup in Altenmünster — we'll be in touch once your order is ready.",
|
||||
giftNote: "🎁 Your personal message will be handwritten on a card and included with your order.",
|
||||
trackOrder: "TRACK ORDER",
|
||||
subject: (orderNumber) => `Order confirmation ${orderNumber} — Wabenhain`,
|
||||
},
|
||||
newsletterConfirm: {
|
||||
preheader: "Please confirm your harvest post subscription.",
|
||||
heading: "Almost there!",
|
||||
intro: "Please confirm your harvest post subscription so we can let you know as soon as new honey is available.",
|
||||
button: "CONFIRM SUBSCRIPTION",
|
||||
ignoreNote: "If you didn't sign up, you can simply ignore this email.",
|
||||
subject: "Please confirm your subscription — Wabenhain harvest post",
|
||||
},
|
||||
passwordReset: {
|
||||
preheader: "Reset your Wabenhain password.",
|
||||
heading: "Reset password",
|
||||
intro: "We received a request to reset the password for your Wabenhain account. This link is valid for one hour.",
|
||||
button: "SET NEW PASSWORD",
|
||||
ignoreNote: "If this wasn't you, you can ignore this email — your password stays unchanged.",
|
||||
subject: "Reset password — Wabenhain",
|
||||
},
|
||||
securityAlert: {
|
||||
preheader: "Security notice for your Wabenhain account.",
|
||||
heading: "Security notice",
|
||||
contactNote: "If this wasn't you, please contact us immediately at [email protected].",
|
||||
subject: "Security notice — Wabenhain",
|
||||
passwordChanged: "The password for your Wabenhain account was just changed. All other signed-in devices have been logged out.",
|
||||
twoFactorDisabled: "Two-factor authentication for your Wabenhain account was just disabled.",
|
||||
accountDeleted: "Your Wabenhain account and all personal data have just been permanently deleted.",
|
||||
},
|
||||
emailVerification: {
|
||||
preheader: "Please confirm your email address.",
|
||||
heading: "Welcome to Wabenhain!",
|
||||
intro: "Please confirm your email address so you can order with your account. This link is valid for 24 hours.",
|
||||
button: "CONFIRM EMAIL ADDRESS",
|
||||
ignoreNote: "If you didn't create this account, you can simply ignore this email.",
|
||||
subject: "Please confirm your email address — Wabenhain",
|
||||
},
|
||||
shipment: {
|
||||
preheader: (orderNumber) => `Your order ${orderNumber} is on its way.`,
|
||||
heading: (firstName) => `Your order is on its way, ${firstName}!`,
|
||||
intro: (orderNumber) => `We've just shipped your order <strong>${orderNumber}</strong>.`,
|
||||
trackButton: "TRACK PACKAGE",
|
||||
subject: (orderNumber) => `Your order ${orderNumber} has shipped — Wabenhain`,
|
||||
},
|
||||
pickupReady: {
|
||||
preheader: (orderNumber) => `Your order ${orderNumber} is ready for pickup.`,
|
||||
heading: (firstName) => `Your order is waiting for you, ${firstName}!`,
|
||||
intro: (orderNumber) => `Your order <strong>${orderNumber}</strong> is ready for pickup.`,
|
||||
subject: (orderNumber) => `Your order ${orderNumber} is ready for pickup — Wabenhain`,
|
||||
},
|
||||
backInStock: {
|
||||
preheader: (name) => `${name} is back in stock.`,
|
||||
heading: "Good news!",
|
||||
intro: (name) => `<strong>${name}</strong> is back in stock.`,
|
||||
button: "ORDER NOW",
|
||||
subject: (name) => `${name} is back in stock — Wabenhain`,
|
||||
},
|
||||
returnAccepted: {
|
||||
preheader: (orderNumber) => `Your return request for ${orderNumber} was accepted.`,
|
||||
heading: (firstName) => `Your return was accepted, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`We've accepted your return request for order <strong>${orderNumber}</strong>. Please send the item back to us — once it arrives, we'll process your refund.`,
|
||||
subject: (orderNumber) => `Return for ${orderNumber} accepted — Wabenhain`,
|
||||
},
|
||||
returnRejected: {
|
||||
preheader: (orderNumber) => `Your return request for ${orderNumber} was declined.`,
|
||||
heading: (firstName) => `Your return request was unfortunately declined, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`We've reviewed your return request for order <strong>${orderNumber}</strong> and are unable to accept it. Feel free to reach out with any questions.`,
|
||||
subject: (orderNumber) => `Return for ${orderNumber} declined — Wabenhain`,
|
||||
},
|
||||
returnRefunded: {
|
||||
preheader: (orderNumber) => `Your order ${orderNumber} has been refunded.`,
|
||||
heading: (firstName) => `Your refund is on its way, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`We've processed the refund for your order <strong>${orderNumber}</strong>. Depending on your payment method, it may take a few business days to arrive.`,
|
||||
subject: (orderNumber) => `Order ${orderNumber} refunded — Wabenhain`,
|
||||
},
|
||||
paymentFailed: {
|
||||
preheader: (orderNumber) => `Payment for order ${orderNumber} could not be completed.`,
|
||||
heading: (firstName) => `Unfortunately your payment didn't go through, ${firstName}.`,
|
||||
intro: (orderNumber) =>
|
||||
`The payment for your order <strong>${orderNumber}</strong> could not be completed. We've cancelled the order, so no costs were incurred.`,
|
||||
retryNote: "Feel free to place the order again, for example with a different payment method.",
|
||||
subject: (orderNumber) => `Payment for order ${orderNumber} failed — Wabenhain`,
|
||||
},
|
||||
};
|
||||
|
||||
const emailStrings: Record<Locale, EmailStrings> = { de, en };
|
||||
|
||||
export function getEmailStrings(locale: Locale): EmailStrings {
|
||||
return emailStrings[locale];
|
||||
}
|
||||
Reference in new issue
Block a user