refactor: /admin/inhalte in Bilder/Text trennen, KI-untypische Formulierungen in Website-Texten
Content-Editor zeigt Bild-/Video-Felder jetzt gebuendelt oben und Textfelder gebuendelt darunter (je Seite ein eigenes Speichern-Formular), statt beides pro Bereich gemischt untereinander. Ausserdem alle Gedankenstrich-Konstruktionen und aehnliche KI-Textmuster aus den Website-Texten (de/en) entfernt und durch natuerlichere Formulierungen ersetzt. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
6814e9e2c8
commit
286387d5ec
5 files changed
+294
-222
No files matched your search
+177
-117
@@ -6,6 +6,8 @@ import { CONTENT_REGISTRY, type ContentFieldDef } from "@/lib/content-registry";
|
||||
import { updateSiteContentAction } from "../actions";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import type { Dictionary } from "@/i18n/dictionary-type";
|
||||
import type { Locale } from "@/i18n/config";
|
||||
|
||||
const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
|
||||
text: 1,
|
||||
@@ -21,11 +23,155 @@ const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
|
||||
video: 0,
|
||||
};
|
||||
|
||||
async function loadCurrentValues(): Promise<Map<string, { valueDe: string; valueEn: string | null }>> {
|
||||
type CurrentValues = Map<string, { valueDe: string; valueEn: string | null }>;
|
||||
|
||||
async function loadCurrentValues(): Promise<CurrentValues> {
|
||||
const rows = await prisma.siteContent.findMany();
|
||||
return new Map(rows.map((r) => [r.key, { valueDe: r.valueDe, valueEn: r.valueEn }]));
|
||||
}
|
||||
|
||||
/** Bild- und Video-Felder gehoeren zum "Bilder"-Bereich, alles andere
|
||||
* (Text/Listen/Rechtstexte) zum "Text"-Bereich — siehe AdminInhaltePage. */
|
||||
function isImageOrVideoField(def: ContentFieldDef): boolean {
|
||||
return def.type === "image" || def.type === "video";
|
||||
}
|
||||
|
||||
function FieldEditor({
|
||||
def,
|
||||
row,
|
||||
locale,
|
||||
t,
|
||||
}: {
|
||||
def: ContentFieldDef;
|
||||
row: { valueDe: string; valueEn: string | null } | undefined;
|
||||
locale: Locale;
|
||||
t: Dictionary;
|
||||
}) {
|
||||
const rows = ROWS_BY_TYPE[def.type];
|
||||
const label = locale === "de" ? def.labelDe : def.labelEn;
|
||||
|
||||
if (!def.localized) {
|
||||
if (def.type === "image") {
|
||||
return <AdminImageField name={def.key} label={label} initialValue={row?.valueDe ?? ""} />;
|
||||
}
|
||||
if (def.type === "video") {
|
||||
return <AdminVideoField name={def.key} label={label} initialValue={row?.valueDe ?? ""} />;
|
||||
}
|
||||
return (
|
||||
<label className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
{def.type === "textarea" ? (
|
||||
<textarea
|
||||
name={def.key}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={def.key}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const isTextarea = def.type !== "text";
|
||||
return (
|
||||
<div className="border-t border-ink/10 pt-5 first:border-t-0 first:pt-0">
|
||||
<p className="text-sm text-ink/70 mb-2">{label}</p>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/70">{t.adminContentPage.deLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`de::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`de::${def.key}`}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/70">{t.adminContentPage.enLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`en::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`en::${def.key}`}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Ein <form> pro Gruppe UND Bereich (Bilder/Text) — siehe AdminInhaltePage.
|
||||
* Zwei Formulare koennen dieselbe group="..." fuers Audit-Log/Reload
|
||||
* mitschicken, das ist unproblematisch: updateSiteContentAction liest pro
|
||||
* Absenden nur die tatsaechlich in DIESEM Formular vorhandenen Registry-
|
||||
* Felder (siehe admin/actions/content.ts), Felder aus dem jeweils anderen
|
||||
* Formular fehlen im FormData schlicht und werden uebersprungen. */
|
||||
function GroupForm({
|
||||
groupHeading,
|
||||
fields,
|
||||
current,
|
||||
groupKeyValue,
|
||||
locale,
|
||||
t,
|
||||
}: {
|
||||
groupHeading: string;
|
||||
fields: ContentFieldDef[];
|
||||
current: CurrentValues;
|
||||
groupKeyValue: string;
|
||||
locale: Locale;
|
||||
t: Dictionary;
|
||||
}) {
|
||||
if (fields.length === 0) return null;
|
||||
return (
|
||||
<form
|
||||
action={updateSiteContentAction}
|
||||
className="mb-10 bg-background border border-ink/10 p-6 space-y-6"
|
||||
>
|
||||
<input type="hidden" name="group" value={groupKeyValue} />
|
||||
<h3 className="text-lg text-forest">{groupHeading}</h3>
|
||||
|
||||
{fields.map((def) => (
|
||||
<FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} />
|
||||
))}
|
||||
|
||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
||||
{t.adminContentPage.save}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminInhaltePage(props: PageProps<"/admin/inhalte">) {
|
||||
const locale = await getLocale();
|
||||
const t = getDictionary(locale);
|
||||
@@ -52,123 +198,37 @@ export default async function AdminInhaltePage(props: PageProps<"/admin/inhalte"
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{groups.map((group) => {
|
||||
const fields = CONTENT_REGISTRY.filter((f) => f.group === group.key);
|
||||
return (
|
||||
<form
|
||||
key={group.key}
|
||||
action={updateSiteContentAction}
|
||||
className="mb-10 bg-background border border-ink/10 p-6 space-y-6"
|
||||
>
|
||||
<input type="hidden" name="group" value={group.key} />
|
||||
<h2 className="text-lg text-forest">{group.heading}</h2>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionImages}</h2>
|
||||
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionImagesIntro}</p>
|
||||
{groups.map((group) => (
|
||||
<GroupForm
|
||||
key={`images-${group.key}`}
|
||||
groupHeading={group.heading}
|
||||
groupKeyValue={group.key}
|
||||
fields={CONTENT_REGISTRY.filter((f) => f.group === group.key && isImageOrVideoField(f))}
|
||||
current={current}
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{fields.map((def) => {
|
||||
const row = current.get(def.key);
|
||||
const rows = ROWS_BY_TYPE[def.type];
|
||||
const label = locale === "de" ? def.labelDe : def.labelEn;
|
||||
|
||||
if (!def.localized) {
|
||||
if (def.type === "image") {
|
||||
return (
|
||||
<AdminImageField
|
||||
key={def.key}
|
||||
name={def.key}
|
||||
label={label}
|
||||
initialValue={row?.valueDe ?? ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (def.type === "video") {
|
||||
return (
|
||||
<AdminVideoField
|
||||
key={def.key}
|
||||
name={def.key}
|
||||
label={label}
|
||||
initialValue={row?.valueDe ?? ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<label key={def.key} className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
{def.type === "textarea" ? (
|
||||
<textarea
|
||||
name={def.key}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={def.key}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const isTextarea = def.type !== "text";
|
||||
return (
|
||||
<div key={def.key} className="border-t border-ink/10 pt-5 first:border-t-0 first:pt-0">
|
||||
<p className="text-sm text-ink/70 mb-2">{label}</p>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/70">{t.adminContentPage.deLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`de::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`de::${def.key}`}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<label className="block text-xs">
|
||||
<span className="text-ink/70">{t.adminContentPage.enLabel}</span>
|
||||
{isTextarea ? (
|
||||
<textarea
|
||||
name={`en::${def.key}`}
|
||||
rows={rows}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`en::${def.key}`}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
||||
{t.adminContentPage.save}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
<section>
|
||||
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionText}</h2>
|
||||
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionTextIntro}</p>
|
||||
{groups.map((group) => (
|
||||
<GroupForm
|
||||
key={`text-${group.key}`}
|
||||
groupHeading={group.heading}
|
||||
groupKeyValue={group.key}
|
||||
fields={CONTENT_REGISTRY.filter((f) => f.group === group.key && !isImageOrVideoField(f))}
|
||||
current={current}
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+52
-48
@@ -103,7 +103,7 @@ export const de: Dictionary = {
|
||||
subtitle: "Die Region Altenmünster und die Westlichen Wälder im Landkreis Augsburg sind der Ursprung jeder Wabenhain-Ernte.",
|
||||
aspects: [
|
||||
{ title: "Landschaft", text: "Wälder, Wiesen und Streuobst prägen das Landschaftsbild rund um Altenmünster." },
|
||||
{ title: "Vegetation", text: "Von Frühblühern über Robinie bis zur Sommerlinde — die Trachtpflanzen wechseln über das Jahr." },
|
||||
{ title: "Vegetation", text: "Von Frühblühern über Robinie bis zur Sommerlinde: Die Trachtpflanzen wechseln über das Jahr." },
|
||||
{ title: "Jahreszeiten", text: "Jede Saison bringt ein eigenes Nektarangebot und damit einen eigenen Honigcharakter." },
|
||||
{ title: "Bienenstände", text: "Unsere Völker stehen an ausgewählten, ruhigen Standorten in der Region." },
|
||||
],
|
||||
@@ -117,7 +117,7 @@ export const de: Dictionary = {
|
||||
"Was als kleine Leidenschaft für die Bienenhaltung begann, ist heute eine eigenständige Imkerei mit klarer Haltung: Qualität vor Masse, Herkunft vor Anonymität.",
|
||||
"Wir arbeiten mit unseren eigenen Völkern in und um Altenmünster, begleiten jede Saison aufmerksam und greifen nur so weit ein, wie es für gesunde Bienenvölker und eine gute Ernte notwendig ist.",
|
||||
"Kleine Chargen sind für uns kein Kompromiss, sondern bewusste Entscheidung: Nur so lässt sich jede Ernte lückenlos dokumentieren, vom Bienenstand bis ins Glas.",
|
||||
"Dahinter steht die Überzeugung, dass Honig mehr ist als ein Süßungsmittel — er ist ein Abbild der Landschaft, in der er entsteht.",
|
||||
"Dahinter steht die Überzeugung, dass Honig mehr ist als ein Süßungsmittel: Er ist ein Abbild der Landschaft, in der er entsteht.",
|
||||
],
|
||||
imageLabel: "Der Imker bei der Arbeit",
|
||||
},
|
||||
@@ -151,7 +151,7 @@ export const de: Dictionary = {
|
||||
},
|
||||
notFoundPage: {
|
||||
eyebrow: "404",
|
||||
title: "Diese Seite gibt es nicht — zumindest nicht mehr.",
|
||||
title: "Diese Seite gibt es nicht, zumindest nicht mehr.",
|
||||
text: "Der Link ist vielleicht veraltet, oder die Seite wurde verschoben. Schau dich im Shop um oder geh zurück zur Startseite.",
|
||||
backToShop: "Zum Shop",
|
||||
backToHome: "Zur Startseite",
|
||||
@@ -182,15 +182,15 @@ export const de: Dictionary = {
|
||||
{
|
||||
heading: "Angaben gemäß § 5 TMG",
|
||||
paragraphs: [
|
||||
"[PLATZHALTER — Name der Inhaberin/des Inhabers bzw. der Geschäftsführung eintragen]",
|
||||
"[PLATZHALTER — Rechtsform eintragen, z.B. Einzelunternehmen/e.K./GmbH. Sofern im Handelsregister eingetragen: Registergericht und Handelsregisternummer ergänzen.]",
|
||||
"[PLATZHALTER: Name der Inhaberin/des Inhabers bzw. der Geschäftsführung eintragen]",
|
||||
"[PLATZHALTER: Rechtsform eintragen, z.B. Einzelunternehmen/e.K./GmbH. Sofern im Handelsregister eingetragen: Registergericht und Handelsregisternummer ergänzen.]",
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Umsatzsteuer-ID",
|
||||
paragraphs: [
|
||||
"[PLATZHALTER] Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz.",
|
||||
"Sofern stattdessen die Kleinunternehmerregelung nach § 19 UStG angewendet wird, entfällt eine Umsatzsteuer-ID — dies ist an dieser Stelle entsprechend zu vermerken (z.B. \"Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.\").",
|
||||
"Sofern stattdessen die Kleinunternehmerregelung nach § 19 UStG angewendet wird, entfällt eine Umsatzsteuer-ID: Dies ist an dieser Stelle entsprechend zu vermerken (z.B. \"Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.\").",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -237,7 +237,7 @@ export const de: Dictionary = {
|
||||
{
|
||||
heading: "2. Hosting",
|
||||
paragraphs: [
|
||||
"Diese Website wird auf eigener, selbst betriebener Server-Infrastruktur gehostet — nicht bei einem externen Hosting-Anbieter. Es besteht daher kein Auftragsverarbeitungsvertrag mit einem externen Hosting-Dienstleister gemäß Art. 28 DSGVO; die Verarbeitung der Server-Logfiles (siehe Punkt 3) erfolgt ausschließlich durch uns selbst als Verantwortlichen.",
|
||||
"Diese Website wird auf eigener, selbst betriebener Server-Infrastruktur gehostet, nicht bei einem externen Hosting-Anbieter. Es besteht daher kein Auftragsverarbeitungsvertrag mit einem externen Hosting-Dienstleister gemäß Art. 28 DSGVO; die Verarbeitung der Server-Logfiles (siehe Punkt 3) erfolgt ausschließlich durch uns selbst als Verantwortlichen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -332,7 +332,7 @@ export const de: Dictionary = {
|
||||
heading: "7. Gewährleistung",
|
||||
paragraphs: [
|
||||
"Es gelten die gesetzlichen Gewährleistungsrechte (§§ 434 ff. BGB). Bei Verbraucher:innen beträgt die Verjährungsfrist für Mängelansprüche zwei Jahre ab Ablieferung der Ware.",
|
||||
"Bei Lebensmitteln wie Honig weist das aufgedruckte Mindesthaltbarkeitsdatum (MHD) darauf hin, bis wann das Produkt bei sachgerechter Lagerung mindestens seine spezifischen Eigenschaften behält — es handelt sich nicht um ein Verfallsdatum und begründet für sich genommen keinen Mangel.",
|
||||
"Bei Lebensmitteln wie Honig weist das aufgedruckte Mindesthaltbarkeitsdatum (MHD) darauf hin, bis wann das Produkt bei sachgerechter Lagerung mindestens seine spezifischen Eigenschaften behält. Es handelt sich nicht um ein Verfallsdatum und begründet für sich genommen keinen Mangel.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -386,7 +386,7 @@ export const de: Dictionary = {
|
||||
tierRange: (min, max, price) => `${min}–${max} Gläser/Artikel: ${price}`,
|
||||
tierMinimum: (min, price) => `Ab ${min} Gläsern/Artikeln: ${price}`,
|
||||
tierFallback: "Die aktuellen Versandkosten werden dir vor Abschluss der Bestellung transparent angezeigt.",
|
||||
pickupNote: "Alternativ kannst du deine Bestellung nach Absprache direkt bei uns in Altenmünster abholen — dabei fallen keine Versandkosten an.",
|
||||
pickupNote: "Alternativ kannst du deine Bestellung nach Absprache direkt bei uns in Altenmünster abholen. Dabei fallen keine Versandkosten an.",
|
||||
paymentHeading: "Zahlungsmethoden",
|
||||
paymentText: "Wir bieten Kreditkarte, SEPA-Lastschrift, Klarna, PayPal, Apple Pay und Google Pay über unseren Zahlungsdienstleister Stripe an. Eine Zahlung per Überweisung ist auf Anfrage möglich.",
|
||||
deliveryTimeHeading: "Lieferzeit",
|
||||
@@ -405,7 +405,7 @@ export const de: Dictionary = {
|
||||
heading: "Technisch notwendige Cookies",
|
||||
paragraphs: [
|
||||
"Diese Cookies sind für den Betrieb der Website erforderlich und können nicht deaktiviert werden. Sie werden nur gesetzt, wenn du sie durch dein Verhalten aktiv auslöst (z.B. Anmeldung im Kundenkonto).",
|
||||
"wabenhain_session — hält dich nach der Anmeldung im Kundenkonto eingeloggt. Läuft nach 30 Tagen automatisch ab. Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO / § 25 Abs. 2 Nr. 2 TTDSG.",
|
||||
"wabenhain_session: Hält dich nach der Anmeldung im Kundenkonto eingeloggt. Läuft nach 30 Tagen automatisch ab. Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO / § 25 Abs. 2 Nr. 2 TTDSG.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -423,7 +423,7 @@ export const de: Dictionary = {
|
||||
{
|
||||
heading: "Externe Inhalte",
|
||||
paragraphs: [
|
||||
"Google-Rezensionen auf unserer Startseite werden serverseitig über die Google Places API abgerufen und angezeigt — dabei wird kein Cookie in deinem Browser gesetzt und es findet keine direkte Verbindung deines Browsers zu Google statt.",
|
||||
"Google-Rezensionen auf unserer Startseite werden serverseitig über die Google Places API abgerufen und angezeigt. Dabei wird kein Cookie in deinem Browser gesetzt, und es findet keine direkte Verbindung deines Browsers zu Google statt.",
|
||||
"Zahlungen werden über Stripe abgewickelt. Während des Bezahlvorgangs kann Stripe eigene, für die Betrugsprävention notwendige Cookies setzen. Es gelten die Datenschutzhinweise von Stripe.",
|
||||
],
|
||||
},
|
||||
@@ -485,7 +485,7 @@ export const de: Dictionary = {
|
||||
body: [
|
||||
"Sammelbienen fliegen Blüten an und nehmen den zuckerhaltigen Nektar mit ihrem Rüssel auf, den sie in einer speziellen Honigblase transportieren. Dabei versetzen körpereigene Enzyme den Nektar bereits mit ersten Stoffen, die die Haltbarkeit verbessern.",
|
||||
"Im Bienenstock wird der Nektar von Stockbienen mehrfach untereinander weitergereicht und dabei weiter angereichert. Anschließend verteilen die Bienen ihn dünn in den Wabenzellen, wo überschüssiges Wasser verdunstet.",
|
||||
"Erst wenn der Wassergehalt auf unter etwa 18 Prozent gesunken ist, verschließen die Bienen die Zelle mit einem Wachsdeckel — der Honig ist reif und kann geerntet werden.",
|
||||
"Erst wenn der Wassergehalt auf unter etwa 18 Prozent gesunken ist, verschließen die Bienen die Zelle mit einem Wachsdeckel. Der Honig ist reif und kann geerntet werden.",
|
||||
],
|
||||
},
|
||||
"warum-schmeckt-jede-honigernte-anders": {
|
||||
@@ -493,7 +493,7 @@ export const de: Dictionary = {
|
||||
excerpt: "Wetter, Blühzeitpunkte und Standort machen jede Charge zu einem eigenen Geschmackserlebnis.",
|
||||
body: [
|
||||
"Honig ist ein Naturprodukt: Sein Geschmack hängt unmittelbar davon ab, welche Pflanzen zum Zeitpunkt der Ernte in der Umgebung der Bienenstöcke geblüht haben.",
|
||||
"Wetterbedingungen wie Regen, Sonnenstunden und Temperatur beeinflussen, wie viel Nektar welche Pflanzenart in einem bestimmten Jahr liefert — und damit, welche Trachtquelle im Honig dominiert.",
|
||||
"Wetterbedingungen wie Regen, Sonnenstunden und Temperatur beeinflussen, wie viel Nektar welche Pflanzenart in einem bestimmten Jahr liefert, und damit, welche Trachtquelle im Honig dominiert.",
|
||||
"Deshalb schmeckt auch dieselbe Sorte, etwa Frühtracht, von Jahr zu Jahr und von Standort zu Standort leicht unterschiedlich. Wir dokumentieren jede Charge einzeln, damit dieser Unterschied nachvollziehbar bleibt statt in einer Mischcharge zu verschwinden.",
|
||||
],
|
||||
},
|
||||
@@ -510,27 +510,27 @@ export const de: Dictionary = {
|
||||
title: "Was macht regionalen Honig aus?",
|
||||
excerpt: "Kurze Wege, bekannte Standorte und nachvollziehbare Ernten unterscheiden regionalen von anonymem Honig.",
|
||||
body: [
|
||||
"Regionaler Honig stammt von Bienenvölkern, deren Standort und Umgebung bekannt sind — anders als bei vielen Honigmischungen aus dem Supermarkt, die Honig aus mehreren Ländern zusammenführen.",
|
||||
"Regionaler Honig stammt von Bienenvölkern, deren Standort und Umgebung bekannt sind, anders als bei vielen Honigmischungen aus dem Supermarkt, die Honig aus mehreren Ländern zusammenführen.",
|
||||
"Das bedeutet kurze Transportwege, eine Ernte, die zur heimischen Landschaft passt, und eine Imkerei, die für Qualität und Herkunft direkt ansprechbar ist.",
|
||||
"Bei Wabenhain lässt sich jede Charge bis zum Bienenstand zurückverfolgen — das ist der Kern dessen, was für uns regionalen Honig ausmacht.",
|
||||
"Bei Wabenhain lässt sich jede Charge bis zum Bienenstand zurückverfolgen: Das ist der Kern dessen, was für uns regionalen Honig ausmacht.",
|
||||
],
|
||||
},
|
||||
"wie-honig-richtig-lagern": {
|
||||
title: "Wie sollte Honig gelagert werden?",
|
||||
excerpt: "Kühl, dunkel und gut verschlossen — so bleibt Honig am längsten in bester Qualität.",
|
||||
excerpt: "Kühl, dunkel und gut verschlossen: So bleibt Honig am längsten in bester Qualität.",
|
||||
body: [
|
||||
"Honig sollte bei möglichst gleichbleibender Temperatur zwischen 10 und 20 Grad gelagert werden, idealerweise in einem Vorratsschrank statt im Kühlschrank — zu kalte Lagerung beschleunigt die Kristallisation, ohne die Qualität zu verbessern.",
|
||||
"Honig sollte bei möglichst gleichbleibender Temperatur zwischen 10 und 20 Grad gelagert werden, idealerweise in einem Vorratsschrank statt im Kühlschrank. Zu kalte Lagerung beschleunigt die Kristallisation, ohne die Qualität zu verbessern.",
|
||||
"Direktes Sonnenlicht und Wärmequellen wie Herd oder Heizung sollten vermieden werden, da Wärme wertvolle Aromen und Inhaltsstoffe abbauen kann.",
|
||||
"Das Glas nach jeder Nutzung gut verschließen, damit kein Wasser aus der Luft in den Honig gelangt — das kann sonst Gärung begünstigen.",
|
||||
"Das Glas nach jeder Nutzung gut verschließen, damit kein Wasser aus der Luft in den Honig gelangt. Das kann sonst Gärung begünstigen.",
|
||||
],
|
||||
},
|
||||
"warum-nie-ueber-28-grad-erwaermt": {
|
||||
title: "Warum wird unser Honig nie über 28 °C erwärmt?",
|
||||
excerpt: "Honig enthält von Natur aus hitzeempfindliche Enzyme — bei uns bleiben sie durch schonende Verarbeitung erhalten.",
|
||||
excerpt: "Honig enthält von Natur aus hitzeempfindliche Enzyme: Bei uns bleiben sie durch schonende Verarbeitung erhalten.",
|
||||
body: [
|
||||
"Honig enthält von den Bienen selbst eingebrachte Enzyme, unter anderem Invertase und Diastase. Sie sind deutlich hitzeempfindlich: Je stärker und länger Honig erwärmt wird, desto mehr geht davon verloren — messbar unter anderem an der sogenannten Diastasezahl und am HMF-Gehalt (Hydroxymethylfurfural), einem anerkannten Indikator für Wärmebelastung.",
|
||||
"Honig enthält von den Bienen selbst eingebrachte Enzyme, unter anderem Invertase und Diastase. Sie sind deutlich hitzeempfindlich: Je stärker und länger Honig erwärmt wird, desto mehr geht davon verloren, messbar unter anderem an der sogenannten Diastasezahl und am HMF-Gehalt (Hydroxymethylfurfural), einem anerkannten Indikator für Wärmebelastung.",
|
||||
"In der Imkerei gilt allgemein eine Grenze von 40 °C, um diese Inhaltsstoffe nicht zu schädigen. Das Schleudern selbst findet ohnehin nur bei Raumtemperatur (ca. 25–30 °C) statt, da Bienenwachs schon ab etwa 50 °C zu schmelzen beginnt.",
|
||||
"Bei Wabenhain gehen wir bewusst noch einen Schritt weiter: Unser Honig wird zu keinem Zeitpunkt der Ernte, Schleuderung und Abfüllung über 28 °C erwärmt — spürbar unterhalb sowohl der üblichen 40-Grad-Grenze als auch der natürlichen Bienenstocktemperatur von rund 35 °C. So bleiben die Enzyme so weit wie möglich erhalten, wie sie auch im Bienenstock vorliegen.",
|
||||
"Bei Wabenhain gehen wir bewusst noch einen Schritt weiter: Unser Honig wird zu keinem Zeitpunkt der Ernte, Schleuderung und Abfüllung über 28 °C erwärmt, spürbar unterhalb sowohl der üblichen 40-Grad-Grenze als auch der natürlichen Bienenstocktemperatur von rund 35 °C. So bleiben die Enzyme so weit wie möglich erhalten, wie sie auch im Bienenstock vorliegen.",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -539,7 +539,7 @@ export const de: Dictionary = {
|
||||
title: "Wir freuen uns von dir zu hören.",
|
||||
pickupHeading: "Honig vor Ort abholen",
|
||||
pickupText:
|
||||
"Nach Absprache kannst du deine Bestellung direkt bei uns in Altenmünster abholen. Wähle dazu im Checkout einfach „Abholung“ aus — wir melden uns anschließend mit einem Abholtermin.",
|
||||
"Nach Absprache kannst du deine Bestellung direkt bei uns in Altenmünster abholen. Wähle dazu im Checkout einfach „Abholung“ aus. Wir melden uns anschließend mit einem Abholtermin.",
|
||||
},
|
||||
twoFactorPage: {
|
||||
eyebrow: "Sicherheit",
|
||||
@@ -569,7 +569,7 @@ export const de: Dictionary = {
|
||||
eyebrow: "Sicherheit",
|
||||
title: "Deine Backup-Codes",
|
||||
subtitle:
|
||||
"Jeder Code funktioniert einmalig, falls du keinen Zugriff auf deine Authenticator-App hast. Speichere sie an einem sicheren Ort — sie werden dir nur jetzt angezeigt.",
|
||||
"Jeder Code funktioniert einmalig, falls du keinen Zugriff auf deine Authenticator-App hast. Speichere sie an einem sicheren Ort. Sie werden dir nur jetzt angezeigt.",
|
||||
warning: "Diese Codes werden nicht erneut angezeigt. Lade die Seite nicht neu, ohne sie gespeichert zu haben.",
|
||||
done: "Fertig",
|
||||
},
|
||||
@@ -650,7 +650,7 @@ export const de: Dictionary = {
|
||||
adminAuditLogPage: {
|
||||
backToDashboard: "Zurück zum Dashboard",
|
||||
title: "Audit-Log",
|
||||
intro: (limit) => `Protokoll aller Admin-Änderungen (Produkte, Bestellungen, Gutscheine, Benutzer, …) — die letzten ${limit} Einträge.`,
|
||||
intro: (limit) => `Protokoll aller Admin-Änderungen (Produkte, Bestellungen, Gutscheine, Benutzer, …), die letzten ${limit} Einträge.`,
|
||||
colTimestamp: "Zeitpunkt",
|
||||
colAdmin: "Admin",
|
||||
colAction: "Aktion",
|
||||
@@ -687,7 +687,7 @@ export const de: Dictionary = {
|
||||
typeFixed: "Fester Betrag (Cent)",
|
||||
valueLabel: "Wert",
|
||||
minOrderLabel: "Mindestbestellwert (Cent, optional)",
|
||||
minQuantityLabel: "Mindestmenge für Mengenrabatt (Stück, optional — automatisch angewendet ohne Codeeingabe)",
|
||||
minQuantityLabel: "Mindestmenge für Mengenrabatt (Stück, optional, automatisch angewendet ohne Codeeingabe)",
|
||||
maxUsesLabel: "Max. Nutzungen insgesamt (leer = unbegrenzt)",
|
||||
validFromLabel: "Gültig ab (optional)",
|
||||
validUntilLabel: "Gültig bis (optional)",
|
||||
@@ -705,7 +705,7 @@ export const de: Dictionary = {
|
||||
typeFixed: "Fester Betrag (Cent)",
|
||||
valueLabel: "Wert",
|
||||
minOrderLabel: "Mindestbestellwert (Cent, optional)",
|
||||
minQuantityLabel: "Mindestmenge für Mengenrabatt (Stück, optional — automatisch angewendet ohne Codeeingabe)",
|
||||
minQuantityLabel: "Mindestmenge für Mengenrabatt (Stück, optional, automatisch angewendet ohne Codeeingabe)",
|
||||
maxUsesLabel: "Max. Nutzungen insgesamt (leer = unbegrenzt)",
|
||||
validFromLabel: "Gültig ab (optional)",
|
||||
validUntilLabel: "Gültig bis (optional)",
|
||||
@@ -717,7 +717,7 @@ export const de: Dictionary = {
|
||||
title: "Versandkosten",
|
||||
newTier: "Neue Staffel",
|
||||
intro:
|
||||
"Die Versandkosten richten sich nach der Gesamtmenge aller Artikel im Warenkorb — mehrere Positionen werden zu einer Menge zusammengezählt, nicht mehr nach Bestellwert berechnet. Ohne passende Staffel greift ein fester Sicherheits-Fallback von 4,90 €. Abholung in Altenmünster ist immer versandkostenfrei.",
|
||||
"Die Versandkosten richten sich nach der Gesamtmenge aller Artikel im Warenkorb. Mehrere Positionen werden zu einer Menge zusammengezählt, nicht mehr nach Bestellwert berechnet. Ohne passende Staffel greift ein fester Sicherheits-Fallback von 4,90 €. Abholung in Altenmünster ist immer versandkostenfrei.",
|
||||
colQuantity: "Menge",
|
||||
colPrice: "Preis",
|
||||
quantityFrom: (min) => `ab ${min} Stück`,
|
||||
@@ -725,7 +725,7 @@ export const de: Dictionary = {
|
||||
edit: "Bearbeiten",
|
||||
deleteConfirm: "Diese Versandstaffel wirklich löschen?",
|
||||
delete: "Löschen",
|
||||
noTiersYet: "Noch keine Staffeln angelegt — es gilt der Sicherheits-Fallback von 4,90 €.",
|
||||
noTiersYet: "Noch keine Staffeln angelegt. Es gilt der Sicherheits-Fallback von 4,90 €.",
|
||||
},
|
||||
adminShippingEditPage: {
|
||||
backToShipping: "Zurück zu Versandkosten",
|
||||
@@ -801,7 +801,7 @@ export const de: Dictionary = {
|
||||
backToUsers: "Zurück zu Benutzern",
|
||||
title: "Neuer Benutzer",
|
||||
intro:
|
||||
"Es wird kein Passwort vergeben — der Benutzer erhält sofort eine E-Mail mit einem Link, um sein eigenes Passwort festzulegen.",
|
||||
"Es wird kein Passwort vergeben: Der Benutzer erhält sofort eine E-Mail mit einem Link, um sein eigenes Passwort festzulegen.",
|
||||
firstNameLabel: "Vorname",
|
||||
lastNameLabel: "Nachname",
|
||||
emailLabel: "E-Mail",
|
||||
@@ -843,7 +843,7 @@ export const de: Dictionary = {
|
||||
trackOrderPage: {
|
||||
eyebrow: "Bestellung verfolgen",
|
||||
title: "Wo ist meine Bestellung?",
|
||||
intro: "Gib deine Bestellnummer und die bei der Bestellung verwendete E-Mail-Adresse ein, um den aktuellen Status zu sehen — auch ohne Kundenkonto.",
|
||||
intro: "Gib deine Bestellnummer und die bei der Bestellung verwendete E-Mail-Adresse ein, um den aktuellen Status zu sehen, auch ohne Kundenkonto.",
|
||||
orderNumberLabel: "Bestellnummer",
|
||||
orderNumberPlaceholder: "z.B. WH-2026-A1B2C3D4",
|
||||
submit: "Status anzeigen",
|
||||
@@ -867,7 +867,7 @@ export const de: Dictionary = {
|
||||
errorPage: {
|
||||
eyebrow: "Fehler",
|
||||
title: "Etwas ist schiefgelaufen.",
|
||||
description: "Das war nicht dein Fehler — bitte versuche es erneut, oder kehre zur Startseite zurück.",
|
||||
description: "Das war nicht dein Fehler. Bitte versuche es erneut, oder kehre zur Startseite zurück.",
|
||||
retry: "Erneut versuchen",
|
||||
backHome: "Zur Startseite",
|
||||
},
|
||||
@@ -897,7 +897,7 @@ export const de: Dictionary = {
|
||||
create: "Anlegen",
|
||||
syncHeading: "ApiCore-Synchronisierung",
|
||||
syncIntro:
|
||||
"Importiert Ernten UND Chargen aus ApiCore. Ernten werden als Entwürfe angelegt (nicht öffentlich sichtbar) — bereits veröffentlichte, zuvor synchronisierte Ernten werden dabei nicht verändert. Chargen werden nur neu angelegt (nie überschrieben, damit echte Lagerbestände erhalten bleiben) und brauchen eine bereits vorhandene Ernte.",
|
||||
"Importiert Ernten UND Chargen aus ApiCore. Ernten werden als Entwürfe angelegt (nicht öffentlich sichtbar); bereits veröffentlichte, zuvor synchronisierte Ernten werden dabei nicht verändert. Chargen werden nur neu angelegt (nie überschrieben, damit echte Lagerbestände erhalten bleiben) und brauchen eine bereits vorhandene Ernte.",
|
||||
syncButton: "Jetzt synchronisieren",
|
||||
syncResult: (created, updated, skipped, failed) =>
|
||||
`Ernten: ${created} neu angelegt, ${updated} aktualisiert, ${skipped} übersprungen (bereits veröffentlicht)${failed > 0 ? `, ${failed} fehlgeschlagen` : ""}.`,
|
||||
@@ -966,7 +966,7 @@ export const de: Dictionary = {
|
||||
categoryHint:
|
||||
"Nur Honig-Produkte sind einer Ernte/Charge zugeordnet. Bei anderen Kategorien wird diese Auswahl beim Speichern automatisch ignoriert, da z.B. Kerzen unabhängig von einer Ernte sind.",
|
||||
harvestLabel: "Ernte",
|
||||
noneOption: "— keine —",
|
||||
noneOption: "Keine Auswahl",
|
||||
batchLabel: "Charge",
|
||||
batchOption: (code, type, year) => `${code} (${type} ${year})`,
|
||||
descriptionLabel: "Beschreibung",
|
||||
@@ -1026,7 +1026,7 @@ export const de: Dictionary = {
|
||||
adminOrderDetailPage: {
|
||||
backToOrders: "Zurück zu Bestellungen",
|
||||
saveStatus: "Status speichern",
|
||||
giftHeading: "🎁 Geschenk — Nachricht beilegen",
|
||||
giftHeading: "🎁 Geschenk: Nachricht beilegen",
|
||||
customerHeading: "Kunde",
|
||||
registeredAccount: "Registriertes Kundenkonto",
|
||||
guestOrder: "Gastbestellung",
|
||||
@@ -1060,7 +1060,7 @@ export const de: Dictionary = {
|
||||
saveTracking: "Speichern",
|
||||
dhlHeading: "DHL-Versandmarke",
|
||||
dhlCreateButton: "Versandmarke erstellen",
|
||||
dhlAlreadyCreated: (trackingNumber) => `DHL-Versandmarke bereits erstellt — Sendungsnummer ${trackingNumber}`,
|
||||
dhlAlreadyCreated: (trackingNumber) => `DHL-Versandmarke bereits erstellt: Sendungsnummer ${trackingNumber}`,
|
||||
dhlNotConfigured: "DHL ist noch nicht konfiguriert, siehe .env (DHL_API_KEY, DHL_API_SECRET, DHL_GKP_USERNAME, DHL_GKP_PASSWORD, DHL_ACCOUNT_NUMBER).",
|
||||
},
|
||||
adminOrdersPage: {
|
||||
@@ -1111,7 +1111,11 @@ export const de: Dictionary = {
|
||||
backToDashboard: "Zurück zum Dashboard",
|
||||
title: "Inhalte",
|
||||
intro:
|
||||
"Diese Texte erscheinen auf der Website. Ein leeres Feld bedeutet nicht \"leer auf der Seite\" — dann wird der ursprüngliche Text weiter angezeigt. Trage nur ein, was du ändern möchtest.",
|
||||
"Diese Texte erscheinen auf der Website. Ein leeres Feld bedeutet nicht \"leer auf der Seite\": Dann wird der ursprüngliche Text weiter angezeigt. Trage nur ein, was du ändern möchtest.",
|
||||
sectionImages: "Bilder",
|
||||
sectionImagesIntro: "Alle Bilder und das Hero-Video, die du hochladen kannst, sortiert nach Seite.",
|
||||
sectionText: "Texte",
|
||||
sectionTextIntro: "Alle Textinhalte, sortiert nach Bereich.",
|
||||
groupCompany: "Firmendaten",
|
||||
groupHome: "Homepage",
|
||||
groupPages: "Weitere Seiten",
|
||||
@@ -1156,7 +1160,7 @@ export const de: Dictionary = {
|
||||
loggedInAs: (name) => `Angemeldet als ${name}`,
|
||||
logout: "Abmelden",
|
||||
twoFactorWarning:
|
||||
"Dieses Admin-Konto hat noch keine Zwei-Faktor-Authentifizierung aktiviert — empfohlen für Konten mit Zugriff auf Bestellungen, Rückerstattungen und Kundendaten.",
|
||||
"Dieses Admin-Konto hat noch keine Zwei-Faktor-Authentifizierung aktiviert, empfohlen für Konten mit Zugriff auf Bestellungen, Rückerstattungen und Kundendaten.",
|
||||
twoFactorSetupLink: "Jetzt einrichten →",
|
||||
},
|
||||
invoicePage: {
|
||||
@@ -1206,7 +1210,7 @@ export const de: Dictionary = {
|
||||
ssoDivider: "oder",
|
||||
ssoButton: "Mit Microsoft anmelden",
|
||||
greeting: (name) => `Hallo, ${name}`,
|
||||
verifyEmailPrompt: "Bitte bestätige deine E-Mail-Adresse — ohne Bestätigung kannst du mit diesem Konto nicht bestellen.",
|
||||
verifyEmailPrompt: "Bitte bestätige deine E-Mail-Adresse. Ohne Bestätigung kannst du mit diesem Konto nicht bestellen.",
|
||||
resendVerification: "Link erneut senden",
|
||||
navOrders: { title: "Bestellungen", description: "Bestellhistorie, Versandstatus und Rechnungen" },
|
||||
navAddresses: { title: "Adressen", description: "Lieferadressen verwalten" },
|
||||
@@ -1249,7 +1253,7 @@ export const de: Dictionary = {
|
||||
backLabel: "Zurück zur Anmeldung",
|
||||
title: "Passwort vergessen",
|
||||
sentMessage: "Falls ein Konto mit dieser E-Mail-Adresse besteht, haben wir dir gerade einen Link zum Zurücksetzen des Passworts geschickt. Der Link ist eine Stunde gültig.",
|
||||
intro: "Gib deine E-Mail-Adresse ein — wir schicken dir einen Link zum Zurücksetzen des Passworts.",
|
||||
intro: "Gib deine E-Mail-Adresse ein. Wir schicken dir einen Link zum Zurücksetzen des Passworts.",
|
||||
requestLink: "Link anfordern",
|
||||
},
|
||||
passwortZuruecksetzenPage: {
|
||||
@@ -1267,7 +1271,7 @@ export const de: Dictionary = {
|
||||
pendingTitle: "Deine Bestellung ist eingegangen.",
|
||||
successTitle: "Deine Bestellung wurde bestätigt.",
|
||||
orderNumberLabel: "Bestellnummer",
|
||||
pendingText: "Die Zahlungsabwicklung war kurzfristig nicht erreichbar — wir melden uns per E-Mail bei dir, um die Zahlung abzuschließen.",
|
||||
pendingText: "Die Zahlungsabwicklung war kurzfristig nicht erreichbar. Wir melden uns per E-Mail bei dir, um die Zahlung abzuschließen.",
|
||||
successText: "Wir bereiten dein Glas Wabenhain-Honig mit Sorgfalt vor. Sobald deine Zahlung eingegangen ist, senden wir dir eine Bestätigung per E-Mail (bei SEPA-Lastschrift kann das einige Werktage dauern).",
|
||||
continueShopping: "Weiter stöbern",
|
||||
trackOrder: "Bestellung verfolgen",
|
||||
@@ -1275,7 +1279,7 @@ export const de: Dictionary = {
|
||||
checkoutCancelPage: {
|
||||
eyebrow: "Abgebrochen",
|
||||
title: "Deine Bestellung wurde nicht abgeschlossen.",
|
||||
text: "Kein Problem — dein Warenkorb ist noch gefüllt. Du kannst den Bestellvorgang jederzeit erneut starten.",
|
||||
text: "Kein Problem, dein Warenkorb ist noch gefüllt. Du kannst den Bestellvorgang jederzeit erneut starten.",
|
||||
backToCart: "Zurück zum Warenkorb",
|
||||
},
|
||||
checkoutPage: {
|
||||
@@ -1302,7 +1306,7 @@ export const de: Dictionary = {
|
||||
submitting: "Wird verarbeitet…",
|
||||
placeOrder: "Kostenpflichtig bestellen",
|
||||
paymentMethodsNote:
|
||||
"Zahlungsmethoden: PayPal, Kreditkarte, Apple Pay, Google Pay, SEPA-Lastschrift, Klarna — bereitgestellt über Stripe.",
|
||||
"Zahlungsmethoden: PayPal, Kreditkarte, Apple Pay, Google Pay, SEPA-Lastschrift, Klarna, bereitgestellt über Stripe.",
|
||||
yourOrderHeading: "Deine Bestellung",
|
||||
shippingWithCount: (count) => `Versand (${count} Stück)`,
|
||||
shippingPickup: "Versand (Abholung)",
|
||||
@@ -1342,7 +1346,7 @@ export const de: Dictionary = {
|
||||
shippingHint: (price) => `Versand ab ${price} · Lieferzeit 2–4 Werktage`,
|
||||
onlyLeftInStock: (count) => `Nur noch ${count} auf Lager`,
|
||||
soldOut: "Ausverkauft",
|
||||
currentlyUnavailable: "Aktuell nicht verfügbar — schau bald wieder vorbei.",
|
||||
currentlyUnavailable: "Aktuell nicht verfügbar, schau bald wieder vorbei.",
|
||||
notifyMeLabel: "Benachrichtige mich, wenn es wieder verfügbar ist",
|
||||
emailPlaceholder: "E-Mail-Adresse",
|
||||
notifySubmit: "Anmelden",
|
||||
@@ -1356,7 +1360,7 @@ export const de: Dictionary = {
|
||||
"Honig aus Deutschland",
|
||||
"abgefüllt in Altenmünster",
|
||||
"nachvollziehbare Charge",
|
||||
"nie über 28 °C erwärmt — Enzyme bleiben erhalten",
|
||||
"nie über 28 °C erwärmt, Enzyme bleiben erhalten",
|
||||
],
|
||||
KERZEN: [
|
||||
"handgegossen aus reinem Bienenwachs",
|
||||
@@ -1387,7 +1391,7 @@ export const de: Dictionary = {
|
||||
noReviewsForProduct: "Für dieses Produkt gibt es noch keine Bewertungen.",
|
||||
loginToReview: "um dieses Produkt zu bewerten.",
|
||||
alreadyReviewed: "Du hast dieses Produkt bereits bewertet.",
|
||||
alreadyReviewedPending: "Du hast dieses Produkt bereits bewertet — deine Bewertung wird noch geprüft.",
|
||||
alreadyReviewedPending: "Du hast dieses Produkt bereits bewertet, deine Bewertung wird noch geprüft.",
|
||||
onlyBuyersCanReview: "Nur Käufer:innen können dieses Produkt bewerten.",
|
||||
yourRating: "Deine Bewertung",
|
||||
commentPlaceholder: "Was hat dir gefallen? (optional)",
|
||||
@@ -1400,7 +1404,7 @@ export const de: Dictionary = {
|
||||
shop: {
|
||||
eyebrow: "Shop",
|
||||
title: "Unser Sortiment",
|
||||
subtitle: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei — solange der jeweilige Bestand reicht.",
|
||||
subtitle: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei, solange der jeweilige Bestand reicht.",
|
||||
allCategories: "Alle",
|
||||
sortLabel: "Sortieren:",
|
||||
sortNewest: "Neu eingetroffen",
|
||||
@@ -1426,7 +1430,7 @@ export const de: Dictionary = {
|
||||
currentHarvest: {
|
||||
eyebrow: "Aktuell verfügbar",
|
||||
title: "Die aktuelle Ernte",
|
||||
subtitle: "Kleine, dokumentierte Chargen — solange der Vorrat reicht.",
|
||||
subtitle: "Kleine, dokumentierte Chargen, solange der Vorrat reicht.",
|
||||
},
|
||||
originStory: {
|
||||
eyebrow: "Herkunft",
|
||||
@@ -1507,7 +1511,7 @@ export const de: Dictionary = {
|
||||
},
|
||||
{ quote: "Man schmeckt wirklich, dass hier jede Charge mit Sorgfalt entsteht.", author: "Markus, Altenmünster" },
|
||||
{
|
||||
quote: "Schönes Geschenk für Kunden — die Rückmeldungen waren durchweg begeistert.",
|
||||
quote: "Schönes Geschenk für Kunden. Die Rückmeldungen waren durchweg begeistert.",
|
||||
author: "Firmenkunde, Region Augsburg",
|
||||
},
|
||||
],
|
||||
|
||||
+60
-56
@@ -103,12 +103,12 @@ export const en: Dictionary = {
|
||||
subtitle: "The Altenmünster region and the Western Woods in the district of Augsburg are the origin of every Wabenhain harvest.",
|
||||
aspects: [
|
||||
{ title: "Landscape", text: "Forests, meadows, and orchard meadows shape the landscape around Altenmünster." },
|
||||
{ title: "Vegetation", text: "From early bloomers to black locust to summer linden — the forage plants change throughout the year." },
|
||||
{ title: "Vegetation", text: "From early bloomers to black locust to summer linden: the forage plants change throughout the year." },
|
||||
{ title: "Seasons", text: "Every season offers its own nectar supply, and with it, its own honey character." },
|
||||
{ title: "Bee sites", text: "Our colonies are located at selected, quiet sites throughout the region." },
|
||||
],
|
||||
privacyNote:
|
||||
"Out of consideration for our bee colonies, we don't show exact locations — only the approximate area of our bee sites.",
|
||||
"Out of consideration for our bee colonies, we don't show exact locations, only the approximate area of our bee sites.",
|
||||
},
|
||||
imkereiPage: {
|
||||
eyebrow: "About Wabenhain",
|
||||
@@ -116,8 +116,8 @@ export const en: Dictionary = {
|
||||
paragraphs: [
|
||||
"What began as a small passion for beekeeping is now a dedicated apiary with a clear stance: quality over volume, origin over anonymity.",
|
||||
"We work with our own colonies in and around Altenmünster, watch over every season closely, and intervene only as much as healthy bee colonies and a good harvest require.",
|
||||
"Small batches aren't a compromise for us — they're a deliberate choice: it's the only way to document every harvest fully, from hive to jar.",
|
||||
"Behind it all is the conviction that honey is more than a sweetener — it's a reflection of the landscape it comes from.",
|
||||
"Small batches are a deliberate choice for us rather than a compromise: it's the only way to document every harvest fully, from hive to jar.",
|
||||
"Behind it all is the conviction that honey is more than a sweetener: it's a reflection of the landscape it comes from.",
|
||||
],
|
||||
imageLabel: "The beekeeper at work",
|
||||
},
|
||||
@@ -151,7 +151,7 @@ export const en: Dictionary = {
|
||||
},
|
||||
notFoundPage: {
|
||||
eyebrow: "404",
|
||||
title: "This page doesn't exist — at least not anymore.",
|
||||
title: "This page doesn't exist, at least not anymore.",
|
||||
text: "The link might be outdated, or the page has moved. Take a look around the shop, or head back to the homepage.",
|
||||
backToShop: "Go to shop",
|
||||
backToHome: "Back to homepage",
|
||||
@@ -182,15 +182,15 @@ export const en: Dictionary = {
|
||||
{
|
||||
heading: "Information pursuant to § 5 TMG (German Telemedia Act)",
|
||||
paragraphs: [
|
||||
"[PLACEHOLDER — enter the name of the owner / managing director]",
|
||||
"[PLACEHOLDER — enter the legal form, e.g. sole proprietorship/GmbH. If registered in the commercial register: add the register court and registration number.]",
|
||||
"[PLACEHOLDER: enter the name of the owner / managing director]",
|
||||
"[PLACEHOLDER: enter the legal form, e.g. sole proprietorship/GmbH. If registered in the commercial register: add the register court and registration number.]",
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "VAT ID",
|
||||
paragraphs: [
|
||||
"[PLACEHOLDER] VAT identification number pursuant to § 27a of the German VAT Act (UStG).",
|
||||
"If the small business regulation under § 19 UStG applies instead, no VAT ID is required — this should be noted here instead (e.g. \"No VAT is charged pursuant to § 19 UStG.\").",
|
||||
"If the small business regulation under § 19 UStG applies instead, no VAT ID is required: this should be noted here instead (e.g. \"No VAT is charged pursuant to § 19 UStG.\").",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -237,7 +237,7 @@ export const en: Dictionary = {
|
||||
{
|
||||
heading: "2. Hosting",
|
||||
paragraphs: [
|
||||
"This website is hosted on our own, self-operated server infrastructure — not by an external hosting provider. There is therefore no data processing agreement with an external hosting provider under Art. 28 GDPR; the server log files described in item 3 are processed exclusively by us as the data controller.",
|
||||
"This website is hosted on our own, self-operated server infrastructure, not by an external hosting provider. There is therefore no data processing agreement with an external hosting provider under Art. 28 GDPR; the server log files described in item 3 are processed exclusively by us as the data controller.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -258,7 +258,7 @@ export const en: Dictionary = {
|
||||
heading: "5. Customer Account",
|
||||
paragraphs: [
|
||||
"If you create a customer account, we store the data you provide (name, email, password encrypted as a hash) as well as any delivery addresses you optionally save, to give you access to your order history, invoices, and a faster checkout. The legal basis is Art. 6(1)(b) GDPR. You can have your account and associated data deleted at any time via an informal request to us.",
|
||||
"A technically necessary cookie is set for login — see our Cookie Policy.",
|
||||
"A technically necessary cookie is set for login; see our Cookie Policy.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -309,7 +309,7 @@ export const en: Dictionary = {
|
||||
},
|
||||
{
|
||||
heading: "3. Prices and Shipping Costs",
|
||||
paragraphs: ["All prices include statutory VAT. Shipping costs are shown transparently at checkout — see also our \"Shipping & Payment\" page."],
|
||||
paragraphs: ["All prices include statutory VAT. Shipping costs are shown transparently at checkout; see also our \"Shipping & Payment\" page."],
|
||||
},
|
||||
{
|
||||
heading: "4. Payment Methods",
|
||||
@@ -332,7 +332,7 @@ export const en: Dictionary = {
|
||||
heading: "7. Warranty",
|
||||
paragraphs: [
|
||||
"Statutory warranty rights apply (§§ 434 et seq. BGB). For consumers, the limitation period for defect claims is two years from delivery of the goods.",
|
||||
"For food products such as honey, the printed best-before date (BBD) indicates the date until which the product retains its specific properties when stored properly — it is not an expiry date and does not, by itself, constitute a defect.",
|
||||
"For food products such as honey, the printed best-before date (BBD) indicates the date until which the product retains its specific properties when stored properly; it is not an expiry date and does not, by itself, constitute a defect.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -386,7 +386,7 @@ export const en: Dictionary = {
|
||||
tierRange: (min, max, price) => `${min}–${max} jars/items: ${price}`,
|
||||
tierMinimum: (min, price) => `From ${min} jars/items: ${price}`,
|
||||
tierFallback: "The current shipping costs will be shown to you transparently before you complete your order.",
|
||||
pickupNote: "Alternatively, you can pick up your order directly from us in Altenmünster by arrangement — no shipping costs apply in that case.",
|
||||
pickupNote: "Alternatively, you can pick up your order directly from us in Altenmünster by arrangement; no shipping costs apply in that case.",
|
||||
paymentHeading: "Payment Methods",
|
||||
paymentText: "We offer credit card, SEPA direct debit, Klarna, PayPal, Apple Pay, and Google Pay via our payment provider Stripe. Payment by bank transfer is available on request.",
|
||||
deliveryTimeHeading: "Delivery Time",
|
||||
@@ -405,7 +405,7 @@ export const en: Dictionary = {
|
||||
heading: "Technically necessary cookies",
|
||||
paragraphs: [
|
||||
"These cookies are required for the website to function and cannot be disabled. They are only set if you actively trigger them through your actions (e.g. logging into your customer account).",
|
||||
"wabenhain_session — keeps you logged into your customer account after signing in. Expires automatically after 30 days. Legal basis: Art. 6(1)(b) GDPR / § 25(2) No. 2 TTDSG.",
|
||||
"wabenhain_session: keeps you logged into your customer account after signing in. Expires automatically after 30 days. Legal basis: Art. 6(1)(b) GDPR / § 25(2) No. 2 TTDSG.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -423,7 +423,7 @@ export const en: Dictionary = {
|
||||
{
|
||||
heading: "External content",
|
||||
paragraphs: [
|
||||
"Google reviews on our homepage are retrieved and displayed server-side via the Google Places API — no cookie is set in your browser, and your browser does not connect directly to Google.",
|
||||
"Google reviews on our homepage are retrieved and displayed server-side via the Google Places API. No cookie is set in your browser, and your browser does not connect directly to Google.",
|
||||
"Payments are processed via Stripe. During the payment process, Stripe may set its own cookies necessary for fraud prevention. Stripe's privacy policy applies.",
|
||||
],
|
||||
},
|
||||
@@ -447,18 +447,18 @@ export const en: Dictionary = {
|
||||
title: "Why does honey crystallize?",
|
||||
excerpt: "Crystallization is a natural process, not a quality defect. Here's why it happens.",
|
||||
body: [
|
||||
"Honey consists mainly of the sugars glucose and fructose, plus water. Glucose tends to crystallize out over time — a completely natural, physical process, not a sign of poor quality.",
|
||||
"Honey consists mainly of the sugars glucose and fructose, plus water. Glucose tends to crystallize out over time, a completely natural, physical process, not a sign of poor quality.",
|
||||
"The higher a honey variety's glucose content, the faster it crystallizes. That's why single-origin honeys can differ noticeably in how quickly they set.",
|
||||
"If you prefer liquid honey, you can gently warm the jar in a water bath at low temperature. Just be careful not to overheat the honey, so its valuable compounds are preserved.",
|
||||
],
|
||||
},
|
||||
"fluessiger-oder-cremiger-honig": {
|
||||
title: "Liquid or creamy honey?",
|
||||
excerpt: "Both textures are pure honey — the difference lies in how they're processed after harvest.",
|
||||
excerpt: "Both textures are pure honey: the difference lies in how they're processed after harvest.",
|
||||
body: [
|
||||
"Whether a honey is liquid or creamy-firm depends on how the particular variety crystallizes and how it's processed after extraction.",
|
||||
"For a fine, creamy texture, the honey is stirred in a controlled way after harvest. This produces many small crystals instead of fewer large ones, giving the honey a smooth, spreadable texture.",
|
||||
"Both are equal in quality and nutritional value — it's purely a matter of taste and use.",
|
||||
"Both are equal in quality and nutritional value: it's purely a matter of taste and use.",
|
||||
],
|
||||
},
|
||||
"was-bedeutet-fruehtracht": {
|
||||
@@ -466,7 +466,7 @@ export const en: Dictionary = {
|
||||
excerpt: "The spring harvest is the year's first, and carries the character of spring.",
|
||||
body: [
|
||||
"“Frühtracht” refers to honey from the year's first harvest cycle. During this time, bees mainly collect nectar from early bloomers such as fruit trees, rapeseed, and dandelion.",
|
||||
"Spring harvest honey is usually mild in flavor, light in color, and — due to the rapeseed content — often crystallizes quickly into a fine, creamy texture.",
|
||||
"Spring harvest honey is usually mild in flavor, light in color, and (due to the rapeseed content) often crystallizes quickly into a fine, creamy texture.",
|
||||
"Every spring harvest tastes different, because weather, bloom timing, and location vary from year to year.",
|
||||
],
|
||||
},
|
||||
@@ -485,7 +485,7 @@ export const en: Dictionary = {
|
||||
body: [
|
||||
"Forager bees visit blossoms and draw up the sugary nectar with their proboscis, carrying it in a special honey stomach. Along the way, enzymes from their own bodies already begin enriching the nectar with substances that improve its shelf life.",
|
||||
"Inside the hive, house bees pass the nectar between one another repeatedly, enriching it further each time. They then spread it thinly across the honeycomb cells, where excess water evaporates.",
|
||||
"Only once the water content has dropped below around 18 percent do the bees seal the cell with a wax cap — the honey is ripe and ready to be harvested.",
|
||||
"Only once the water content has dropped below around 18 percent do the bees seal the cell with a wax cap. The honey is ripe and ready to be harvested.",
|
||||
],
|
||||
},
|
||||
"warum-schmeckt-jede-honigernte-anders": {
|
||||
@@ -493,7 +493,7 @@ export const en: Dictionary = {
|
||||
excerpt: "Weather, bloom timing, and location make every batch its own flavor experience.",
|
||||
body: [
|
||||
"Honey is a natural product: its flavor depends directly on which plants were blooming near the hives at harvest time.",
|
||||
"Weather conditions like rain, hours of sunshine, and temperature affect how much nectar a given plant species produces in a particular year — and therefore which forage source ends up dominating the honey.",
|
||||
"Weather conditions like rain, hours of sunshine, and temperature affect how much nectar a given plant species produces in a particular year, and therefore which forage source ends up dominating the honey.",
|
||||
"That's why even the same variety, spring harvest for instance, tastes slightly different from year to year and from location to location. We document every batch individually, so this difference stays traceable instead of disappearing into a blended batch.",
|
||||
],
|
||||
},
|
||||
@@ -510,27 +510,27 @@ export const en: Dictionary = {
|
||||
title: "What defines regional honey?",
|
||||
excerpt: "Short distances, known locations, and traceable harvests set regional honey apart from anonymous honey.",
|
||||
body: [
|
||||
"Regional honey comes from bee colonies whose location and surroundings are known — unlike many supermarket honey blends, which combine honey from several countries.",
|
||||
"Regional honey comes from bee colonies whose location and surroundings are known, unlike many supermarket honey blends, which combine honey from several countries.",
|
||||
"That means short transport distances, a harvest that fits the local landscape, and an apiary you can speak to directly about quality and origin.",
|
||||
"At Wabenhain, every batch can be traced back to the hive — that's the core of what regional honey means to us.",
|
||||
"At Wabenhain, every batch can be traced back to the hive: that's the core of what regional honey means to us.",
|
||||
],
|
||||
},
|
||||
"wie-honig-richtig-lagern": {
|
||||
title: "How should honey be stored?",
|
||||
excerpt: "Cool, dark, and well sealed — that's how honey stays at its best for longest.",
|
||||
excerpt: "Cool, dark, and well sealed: that's how honey stays at its best for longest.",
|
||||
body: [
|
||||
"Honey should be stored at as steady a temperature as possible, between 10 and 20 degrees Celsius, ideally in a pantry rather than the fridge — storage that's too cold speeds up crystallization without improving quality.",
|
||||
"Honey should be stored at as steady a temperature as possible, between 10 and 20 degrees Celsius, ideally in a pantry rather than the fridge. Storage that's too cold speeds up crystallization without improving quality.",
|
||||
"Direct sunlight and heat sources like the stove or a radiator should be avoided, since heat can break down valuable aromas and nutrients.",
|
||||
"Reseal the jar tightly after every use, so no moisture from the air gets into the honey — that can otherwise encourage fermentation.",
|
||||
"Reseal the jar tightly after every use, so no moisture from the air gets into the honey. That can otherwise encourage fermentation.",
|
||||
],
|
||||
},
|
||||
"warum-nie-ueber-28-grad-erwaermt": {
|
||||
title: "Why is our honey never heated above 28 °C?",
|
||||
excerpt: "Honey naturally contains heat-sensitive enzymes — ours stay intact thanks to gentle processing.",
|
||||
excerpt: "Honey naturally contains heat-sensitive enzymes: ours stay intact thanks to gentle processing.",
|
||||
body: [
|
||||
"Honey contains enzymes introduced by the bees themselves, including invertase and diastase. They're distinctly heat-sensitive: the more intensely and the longer honey is heated, the more of them are lost — measurable, among other things, by the so-called diastase number and by HMF content (hydroxymethylfurfural), a recognized indicator of heat exposure.",
|
||||
"Honey contains enzymes introduced by the bees themselves, including invertase and diastase. They're distinctly heat-sensitive: the more intensely and the longer honey is heated, the more of them are lost, measurable, among other things, by the so-called diastase number and by HMF content (hydroxymethylfurfural), a recognized indicator of heat exposure.",
|
||||
"Beekeeping generally treats 40 °C as an upper limit to avoid damaging these compounds. Extraction itself already happens at room temperature (roughly 25–30 °C), since beeswax starts to melt at around 50 °C.",
|
||||
"At Wabenhain, we go a deliberate step further: our honey is never heated above 28 °C at any point during harvesting, extraction, or bottling — noticeably below both the usual 40-degree limit and the natural hive temperature of around 35 °C. That keeps the enzymes as close as possible to how they exist in the hive itself.",
|
||||
"At Wabenhain, we go a deliberate step further: our honey is never heated above 28 °C at any point during harvesting, extraction, or bottling, noticeably below both the usual 40-degree limit and the natural hive temperature of around 35 °C. That keeps the enzymes as close as possible to how they exist in the hive itself.",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -539,7 +539,7 @@ export const en: Dictionary = {
|
||||
title: "We'd love to hear from you.",
|
||||
pickupHeading: "Pick up honey locally",
|
||||
pickupText:
|
||||
"By arrangement, you can pick up your order directly from us in Altenmünster. Just select “Pickup” at checkout — we'll then get in touch with a pickup time.",
|
||||
"By arrangement, you can pick up your order directly from us in Altenmünster. Just select “Pickup” at checkout. We'll then get in touch with a pickup time.",
|
||||
},
|
||||
twoFactorPage: {
|
||||
eyebrow: "Security",
|
||||
@@ -569,7 +569,7 @@ export const en: Dictionary = {
|
||||
eyebrow: "Security",
|
||||
title: "Your backup codes",
|
||||
subtitle:
|
||||
"Each code works once, in case you don't have access to your authenticator app. Store them somewhere safe — they're only shown now.",
|
||||
"Each code works once, in case you don't have access to your authenticator app. Store them somewhere safe. They're only shown now.",
|
||||
warning: "These codes won't be shown again. Don't reload the page without saving them.",
|
||||
done: "Done",
|
||||
},
|
||||
@@ -650,7 +650,7 @@ export const en: Dictionary = {
|
||||
adminAuditLogPage: {
|
||||
backToDashboard: "Back to dashboard",
|
||||
title: "Audit log",
|
||||
intro: (limit) => `Log of all admin changes (products, orders, vouchers, users, …) — the last ${limit} entries.`,
|
||||
intro: (limit) => `Log of all admin changes (products, orders, vouchers, users, …), the last ${limit} entries.`,
|
||||
colTimestamp: "Time",
|
||||
colAdmin: "Admin",
|
||||
colAction: "Action",
|
||||
@@ -687,7 +687,7 @@ export const en: Dictionary = {
|
||||
typeFixed: "Fixed amount (cents)",
|
||||
valueLabel: "Value",
|
||||
minOrderLabel: "Min. order value (cents, optional)",
|
||||
minQuantityLabel: "Minimum quantity for bulk discount (items, optional — applied automatically, no code needed)",
|
||||
minQuantityLabel: "Minimum quantity for bulk discount (items, optional, applied automatically, no code needed)",
|
||||
maxUsesLabel: "Max. total uses (empty = unlimited)",
|
||||
validFromLabel: "Valid from (optional)",
|
||||
validUntilLabel: "Valid until (optional)",
|
||||
@@ -705,7 +705,7 @@ export const en: Dictionary = {
|
||||
typeFixed: "Fixed amount (cents)",
|
||||
valueLabel: "Value",
|
||||
minOrderLabel: "Min. order value (cents, optional)",
|
||||
minQuantityLabel: "Minimum quantity for bulk discount (items, optional — applied automatically, no code needed)",
|
||||
minQuantityLabel: "Minimum quantity for bulk discount (items, optional, applied automatically, no code needed)",
|
||||
maxUsesLabel: "Max. total uses (empty = unlimited)",
|
||||
validFromLabel: "Valid from (optional)",
|
||||
validUntilLabel: "Valid until (optional)",
|
||||
@@ -717,7 +717,7 @@ export const en: Dictionary = {
|
||||
title: "Shipping costs",
|
||||
newTier: "New tier",
|
||||
intro:
|
||||
"Shipping cost is based on the total quantity of all items in the cart — multiple line items are summed into one quantity rather than priced by order value. Without a matching tier, a fixed fallback of €4.90 applies. Pickup in Altenmünster is always free.",
|
||||
"Shipping cost is based on the total quantity of all items in the cart. Multiple line items are summed into one quantity rather than priced by order value. Without a matching tier, a fixed fallback of €4.90 applies. Pickup in Altenmünster is always free.",
|
||||
colQuantity: "Quantity",
|
||||
colPrice: "Price",
|
||||
quantityFrom: (min) => `from ${min} units`,
|
||||
@@ -725,7 +725,7 @@ export const en: Dictionary = {
|
||||
edit: "Edit",
|
||||
deleteConfirm: "Really delete this shipping tier?",
|
||||
delete: "Delete",
|
||||
noTiersYet: "No tiers created yet — the €4.90 fallback applies.",
|
||||
noTiersYet: "No tiers created yet. The €4.90 fallback applies.",
|
||||
},
|
||||
adminShippingEditPage: {
|
||||
backToShipping: "Back to shipping costs",
|
||||
@@ -801,7 +801,7 @@ export const en: Dictionary = {
|
||||
backToUsers: "Back to users",
|
||||
title: "New user",
|
||||
intro:
|
||||
"No password is set — the user immediately receives an email with a link to set their own password.",
|
||||
"No password is set: the user immediately receives an email with a link to set their own password.",
|
||||
firstNameLabel: "First name",
|
||||
lastNameLabel: "Last name",
|
||||
emailLabel: "Email",
|
||||
@@ -843,7 +843,7 @@ export const en: Dictionary = {
|
||||
trackOrderPage: {
|
||||
eyebrow: "Track order",
|
||||
title: "Where's my order?",
|
||||
intro: "Enter your order number and the email address used at checkout to see the current status — no account needed.",
|
||||
intro: "Enter your order number and the email address used at checkout to see the current status, no account needed.",
|
||||
orderNumberLabel: "Order number",
|
||||
orderNumberPlaceholder: "e.g. WH-2026-A1B2C3D4",
|
||||
submit: "Show status",
|
||||
@@ -867,7 +867,7 @@ export const en: Dictionary = {
|
||||
errorPage: {
|
||||
eyebrow: "Error",
|
||||
title: "Something went wrong.",
|
||||
description: "This wasn't your fault — please try again, or head back to the homepage.",
|
||||
description: "This wasn't your fault. Please try again, or head back to the homepage.",
|
||||
retry: "Try again",
|
||||
backHome: "Back to homepage",
|
||||
},
|
||||
@@ -897,7 +897,7 @@ export const en: Dictionary = {
|
||||
create: "Create",
|
||||
syncHeading: "ApiCore sync",
|
||||
syncIntro:
|
||||
"Imports harvests AND batches from ApiCore. Harvests are created as drafts (not publicly visible) — previously synced harvests that are already published are left untouched. Batches are only ever created, never updated (so real stock levels are never overwritten), and need an already-existing harvest.",
|
||||
"Imports harvests AND batches from ApiCore. Harvests are created as drafts (not publicly visible); previously synced harvests that are already published are left untouched. Batches are only ever created, never updated (so real stock levels are never overwritten), and need an already-existing harvest.",
|
||||
syncButton: "Sync now",
|
||||
syncResult: (created, updated, skipped, failed) =>
|
||||
`Harvests: ${created} created, ${updated} updated, ${skipped} skipped (already published)${failed > 0 ? `, ${failed} failed` : ""}.`,
|
||||
@@ -966,7 +966,7 @@ export const en: Dictionary = {
|
||||
categoryHint:
|
||||
"Only honey products are linked to a harvest/batch. For other categories this selection is automatically ignored on save, since e.g. candles aren't tied to a harvest.",
|
||||
harvestLabel: "Harvest",
|
||||
noneOption: "— none —",
|
||||
noneOption: "None selected",
|
||||
batchLabel: "Batch",
|
||||
batchOption: (code, type, year) => `${code} (${type} ${year})`,
|
||||
descriptionLabel: "Description",
|
||||
@@ -1026,7 +1026,7 @@ export const en: Dictionary = {
|
||||
adminOrderDetailPage: {
|
||||
backToOrders: "Back to orders",
|
||||
saveStatus: "Save status",
|
||||
giftHeading: "🎁 Gift — enclose message",
|
||||
giftHeading: "🎁 Gift: enclose message",
|
||||
customerHeading: "Customer",
|
||||
registeredAccount: "Registered customer account",
|
||||
guestOrder: "Guest order",
|
||||
@@ -1060,7 +1060,7 @@ export const en: Dictionary = {
|
||||
saveTracking: "Save",
|
||||
dhlHeading: "DHL shipping label",
|
||||
dhlCreateButton: "Create shipping label",
|
||||
dhlAlreadyCreated: (trackingNumber) => `DHL shipping label already created — tracking number ${trackingNumber}`,
|
||||
dhlAlreadyCreated: (trackingNumber) => `DHL shipping label already created: tracking number ${trackingNumber}`,
|
||||
dhlNotConfigured: "DHL isn't configured yet, see .env (DHL_API_KEY, DHL_API_SECRET, DHL_GKP_USERNAME, DHL_GKP_PASSWORD, DHL_ACCOUNT_NUMBER).",
|
||||
},
|
||||
adminOrdersPage: {
|
||||
@@ -1111,7 +1111,11 @@ export const en: Dictionary = {
|
||||
backToDashboard: "Back to dashboard",
|
||||
title: "Content",
|
||||
intro:
|
||||
"These texts appear on the website. An empty field does not mean \"blank on the page\" — the original text keeps showing. Only fill in what you want to change.",
|
||||
"These texts appear on the website. An empty field does not mean \"blank on the page\": the original text keeps showing. Only fill in what you want to change.",
|
||||
sectionImages: "Images",
|
||||
sectionImagesIntro: "Every image and the hero video you can upload, grouped by page.",
|
||||
sectionText: "Text",
|
||||
sectionTextIntro: "Every text field, grouped by section.",
|
||||
groupCompany: "Company details",
|
||||
groupHome: "Homepage",
|
||||
groupPages: "Other pages",
|
||||
@@ -1156,7 +1160,7 @@ export const en: Dictionary = {
|
||||
loggedInAs: (name) => `Signed in as ${name}`,
|
||||
logout: "Sign out",
|
||||
twoFactorWarning:
|
||||
"This admin account doesn't have two-factor authentication enabled yet — recommended for accounts with access to orders, refunds, and customer data.",
|
||||
"This admin account doesn't have two-factor authentication enabled yet, recommended for accounts with access to orders, refunds, and customer data.",
|
||||
twoFactorSetupLink: "Set up now →",
|
||||
},
|
||||
invoicePage: {
|
||||
@@ -1206,7 +1210,7 @@ export const en: Dictionary = {
|
||||
ssoDivider: "or",
|
||||
ssoButton: "Sign in with Microsoft",
|
||||
greeting: (name) => `Hello, ${name}`,
|
||||
verifyEmailPrompt: "Please confirm your email address — you can't place orders with this account until you do.",
|
||||
verifyEmailPrompt: "Please confirm your email address. You can't place orders with this account until you do.",
|
||||
resendVerification: "Resend link",
|
||||
navOrders: { title: "Orders", description: "Order history, shipping status, and invoices" },
|
||||
navAddresses: { title: "Addresses", description: "Manage delivery addresses" },
|
||||
@@ -1249,7 +1253,7 @@ export const en: Dictionary = {
|
||||
backLabel: "Back to sign in",
|
||||
title: "Forgot Password",
|
||||
sentMessage: "If an account exists for this email address, we've just sent a password reset link to it. The link is valid for one hour.",
|
||||
intro: "Enter your email address — we'll send you a link to reset your password.",
|
||||
intro: "Enter your email address. We'll send you a link to reset your password.",
|
||||
requestLink: "Request link",
|
||||
},
|
||||
passwortZuruecksetzenPage: {
|
||||
@@ -1267,7 +1271,7 @@ export const en: Dictionary = {
|
||||
pendingTitle: "Your order has been received.",
|
||||
successTitle: "Your order has been confirmed.",
|
||||
orderNumberLabel: "Order number",
|
||||
pendingText: "Payment processing was briefly unreachable — we'll email you to complete the payment.",
|
||||
pendingText: "Payment processing was briefly unreachable. We'll email you to complete the payment.",
|
||||
successText: "We're carefully preparing your jar of Wabenhain honey. As soon as your payment arrives, we'll send you a confirmation by email (this can take a few business days for SEPA direct debit).",
|
||||
continueShopping: "Keep browsing",
|
||||
trackOrder: "Track order",
|
||||
@@ -1275,7 +1279,7 @@ export const en: Dictionary = {
|
||||
checkoutCancelPage: {
|
||||
eyebrow: "Cancelled",
|
||||
title: "Your order was not completed.",
|
||||
text: "No problem — your cart is still full. You can restart the checkout process at any time.",
|
||||
text: "No problem, your cart is still full. You can restart the checkout process at any time.",
|
||||
backToCart: "Back to cart",
|
||||
},
|
||||
checkoutPage: {
|
||||
@@ -1302,7 +1306,7 @@ export const en: Dictionary = {
|
||||
submitting: "Processing…",
|
||||
placeOrder: "Place order",
|
||||
paymentMethodsNote:
|
||||
"Payment methods: PayPal, credit card, Apple Pay, Google Pay, SEPA direct debit, Klarna — provided via Stripe.",
|
||||
"Payment methods: PayPal, credit card, Apple Pay, Google Pay, SEPA direct debit, Klarna, provided via Stripe.",
|
||||
yourOrderHeading: "Your Order",
|
||||
shippingWithCount: (count) => `Shipping (${count} item${count === 1 ? "" : "s"})`,
|
||||
shippingPickup: "Shipping (pickup)",
|
||||
@@ -1342,7 +1346,7 @@ export const en: Dictionary = {
|
||||
shippingHint: (price) => `Shipping from ${price} · Delivery in 2–4 business days`,
|
||||
onlyLeftInStock: (count) => `Only ${count} in stock`,
|
||||
soldOut: "Sold out",
|
||||
currentlyUnavailable: "Currently unavailable — check back soon.",
|
||||
currentlyUnavailable: "Currently unavailable, check back soon.",
|
||||
notifyMeLabel: "Notify me when it's back in stock",
|
||||
emailPlaceholder: "Email address",
|
||||
notifySubmit: "Sign up",
|
||||
@@ -1356,7 +1360,7 @@ export const en: Dictionary = {
|
||||
"honey from Germany",
|
||||
"bottled in Altenmünster",
|
||||
"traceable batch",
|
||||
"never heated above 28 °C — enzymes stay intact",
|
||||
"never heated above 28 °C, enzymes stay intact",
|
||||
],
|
||||
KERZEN: [
|
||||
"hand-poured from pure beeswax",
|
||||
@@ -1387,7 +1391,7 @@ export const en: Dictionary = {
|
||||
noReviewsForProduct: "There are no reviews for this product yet.",
|
||||
loginToReview: "to review this product.",
|
||||
alreadyReviewed: "You've already reviewed this product.",
|
||||
alreadyReviewedPending: "You've already reviewed this product — your review is still being checked.",
|
||||
alreadyReviewedPending: "You've already reviewed this product. Your review is still being checked.",
|
||||
onlyBuyersCanReview: "Only customers who purchased this product can review it.",
|
||||
yourRating: "Your rating",
|
||||
commentPlaceholder: "What did you like? (optional)",
|
||||
@@ -1400,7 +1404,7 @@ export const en: Dictionary = {
|
||||
shop: {
|
||||
eyebrow: "Shop",
|
||||
title: "Our Range",
|
||||
subtitle: "Honey, beeswax candles, and propolis from our own apiary — while stocks last.",
|
||||
subtitle: "Honey, beeswax candles, and propolis from our own apiary, while stocks last.",
|
||||
allCategories: "All",
|
||||
sortLabel: "Sort by:",
|
||||
sortNewest: "Newest arrivals",
|
||||
@@ -1426,7 +1430,7 @@ export const en: Dictionary = {
|
||||
currentHarvest: {
|
||||
eyebrow: "Currently available",
|
||||
title: "The current harvest",
|
||||
subtitle: "Small, documented batches — while stocks last.",
|
||||
subtitle: "Small, documented batches, while stocks last.",
|
||||
},
|
||||
originStory: {
|
||||
eyebrow: "Origin",
|
||||
@@ -1506,7 +1510,7 @@ export const en: Dictionary = {
|
||||
},
|
||||
{ quote: "You can really taste that every batch is made with care.", author: "Markus, Altenmünster" },
|
||||
{
|
||||
quote: "A lovely gift for clients — the feedback was consistently enthusiastic.",
|
||||
quote: "A lovely gift for clients. The feedback was consistently enthusiastic.",
|
||||
author: "Business customer, Augsburg region",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -811,6 +811,10 @@ export interface Dictionary {
|
||||
backToDashboard: string;
|
||||
title: string;
|
||||
intro: string;
|
||||
sectionImages: string;
|
||||
sectionImagesIntro: string;
|
||||
sectionText: string;
|
||||
sectionTextIntro: string;
|
||||
groupCompany: string;
|
||||
groupHome: string;
|
||||
groupPages: string;
|
||||
|
||||
@@ -84,7 +84,7 @@ export const CONTENT_REGISTRY: ContentFieldDef[] = [
|
||||
// app/api/admin/upload-video/route.ts). Wenn gepflegt, zeigt hero.tsx das
|
||||
// Video statt des Hero-Bilds/Platzhalters an derselben Stelle — home.hero.image
|
||||
// bleibt als Fallback/Poster-Bild erhalten, falls das Video wieder entfernt wird.
|
||||
{ key: "home.hero.video", group: "home", labelDe: "Hero: Hintergrundvideo (MP4/WebM, optional — ersetzt das Hero-Bild)", labelEn: "Hero: Background video (MP4/WebM, optional — replaces the hero image)", type: "video", localized: false, getDefault: () => "" },
|
||||
{ key: "home.hero.video", group: "home", labelDe: "Hero: Hintergrundvideo (MP4/WebM, optional, ersetzt das Hero-Bild)", labelEn: "Hero: Background video (MP4/WebM, optional, replaces the hero image)", type: "video", localized: false, getDefault: () => "" },
|
||||
{ key: "home.originStory.image", group: "home", labelDe: "Herkunft: Bild", labelEn: "Origin: Image", type: "image", localized: false, getDefault: () => "" },
|
||||
{ key: "home.regionalSection.image", group: "home", labelDe: "Regional: Bild", labelEn: "Regional: Image", type: "image", localized: false, getDefault: () => "" },
|
||||
{ key: "home.businessTeaser.image", group: "home", labelDe: "Geschäftskunden-Hinweis: Bild", labelEn: "Business teaser: Image", type: "image", localized: false, getDefault: () => "" },
|
||||
|
||||
Reference in new issue
Block a user