i18n-Fundament: Cookie-basierte Sprachumschaltung (DE/EN/PL)

Wörterbücher unter lib/i18n statt next-intl (Next.js 16 ist neu genug,
dass eine externe Routing-Middleware zusätzliches Risiko bedeutet
hätte); Sprache liegt in einem Cookie, kein URL-Präfix nötig, dadurch
bleibt proxy.ts unverändert. LocaleProvider stellt Wörterbuch + Sprache
per Context für Client-Komponenten bereit (lib/i18n/shared.ts ist
bewusst getrennt von lib/i18n/index.ts, damit next/headers nicht in
den Client-Bundle gelangt).
This commit is contained in:
2026-07-18 02:08:01 +02:00
parent d96faad4d5
commit 33909c660b
8 changed files with 912 additions and 7 deletions
+719
View File
@@ -0,0 +1,719 @@
// Übersetzungswörterbücher für die komplette Oberfläche.
// "de" ist die Referenzstruktur; en/pl werden dagegen typgeprüft,
// damit kein Schlüssel in einer Sprache fehlt.
const de = {
common: {
save: "Speichern",
cancel: "Abbrechen",
delete: "Löschen",
edit: "Bearbeiten",
add: "Hinzufügen",
back: "Zurück",
login: "Anmelden",
logout: "Abmelden",
loading: "Lade…",
you: "(du)",
name: "Name",
email: "E-Mail-Adresse",
password: "Passwort",
role: "Rolle",
roleAdmin: "Administrator",
roleStaff: "Kantinenmitarbeiter",
},
weekdays: {
long: ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"],
short: ["Mo", "Di", "Mi", "Do", "Fr"],
},
dateLocale: "de-DE",
header: {
canteen: "Kantine",
menuGroup: "Speiseplan",
weekPlan: "Wochenplan",
dishes: "Gerichte",
labels: "Allergene & Zusatzstoffe",
adminGroup: "Verwaltung",
users: "Benutzer",
design: "Design",
suggestions: "Vorschläge",
viewMenu: "Speisekarte ansehen",
myAccount: "Mein Konto",
},
login: {
title: "Anmeldung",
submit: "Anmelden",
or: "oder",
passkeyButton: "Mit Passkey anmelden",
backToMenu: "← Zur Speisekarte",
errorInvalidCredentials: "E-Mail-Adresse oder Passwort ist falsch.",
errorEmailNotConfirmed: "Die E-Mail-Adresse wurde noch nicht bestätigt.",
errorPasskeyCancelled: "Passkey-Anmeldung wurde abgebrochen.",
errorGeneric: "Anmeldung fehlgeschlagen: {message}",
errorPasswordDisabled:
"Die Anmeldung mit Passwort ist für dieses Konto deaktiviert. Bitte mit Passkey anmelden.",
},
publicMenu: {
prevWeek: "← Vorherige Woche",
nextWeek: "Nächste Woche →",
prevWeekShort: "← Woche",
nextWeekShort: "Woche →",
today: "Heute",
dayTab: "Tag",
weekTab: "Woche",
allergens: "Allergene",
additives: "Zusatzstoffe",
suggestDish: "Rezept vorschlagen",
noEntry: "–",
},
weekEditor: {
title: "Wochenplan",
loading: "Lade Wochenplan…",
selectDish: "– Gericht wählen –",
addMeal: "Mahlzeit hinzufügen",
newDishesHint: "Neue Gerichte legst du unter {link} an.",
newDishesLinkLabel: "Gerichte",
loadError: "Laden fehlgeschlagen: {message}",
duplicateEntry: "Dieses Gericht ist an diesem Tag bereits zugewiesen.",
saveError: "Speichern fehlgeschlagen: {message}",
deleteError: "Löschen fehlgeschlagen: {message}",
},
dishManager: {
title: "Gerichte",
newDish: "Neues Gericht",
editDish: "Gericht bearbeiten",
nameLabel: "Bezeichnung",
nameEnLabel: "Name (Englisch)",
namePlLabel: "Name (Polnisch)",
autoTranslate: "Automatisch übersetzen",
translating: "Übersetze…",
translated: "Übersetzt.",
translateUnavailable:
"Automatische Übersetzung ist nicht eingerichtet. Bitte Englisch/Polnisch manuell ausfüllen.",
priceLabel: "Preis in €",
pricePlaceholder: "3,50",
searchPlaceholder: "Gericht suchen…",
selectAll: "Alle auswählen",
selectedCount: "{count} ausgewählt",
deleteSelected: "Ausgewählte löschen",
deleteSelectedCount: "Ausgewählte löschen ({count})",
empty: "Noch keine Gerichte angelegt.",
noResults: "Keine Treffer für „{query}“.",
confirmDelete:
"„{name}“ wirklich löschen? Das Gericht wird auch aus allen Wochenplänen entfernt.",
confirmDeleteSelected:
"{count} Gericht(e) wirklich löschen? Sie werden auch aus allen Wochenplänen entfernt.",
invalidPrice: "Bitte einen gültigen Preis angeben, z. B. 3,50",
loadError: "Laden fehlgeschlagen: {message}",
saveError: "Speichern fehlgeschlagen: {message}",
deleteError: "Löschen fehlgeschlagen: {message}",
},
codeList: {
allergensTitle: "Allergene",
additivesTitle: "Zusatzstoffe",
allergenCodeLabel: "Kürzel (z. B. A)",
additiveCodeLabel: "Kürzel (z. B. 1)",
nameLabel: "Bezeichnung",
empty: "Keine Einträge.",
confirmDelete: "„{code} – {name}“ wirklich löschen?",
duplicateCode: "Das Kürzel „{code}“ existiert bereits.",
loadError: "Laden fehlgeschlagen: {message}",
saveError: "Speichern fehlgeschlagen: {message}",
deleteError: "Löschen fehlgeschlagen: {message}",
},
userManager: {
title: "Benutzer",
newUser: "Neuer Benutzer",
editUser: "Benutzer bearbeiten",
newPasswordLabel: "Neues Passwort (optional)",
repeatPasswordLabel: "Passwort wiederholen",
passwordMismatch: "Die Passwörter stimmen nicht überein.",
confirmDelete:
"Benutzer „{email}“ wirklich löschen? Der Zugang wird dauerhaft entfernt.",
loadError: "Laden fehlgeschlagen: {message}",
},
designEditor: {
title: "Design",
logoTitle: "Logo",
logoHint: "Das Logo wird zentriert über der Speisekarte angezeigt.",
logoNone: "Kein Logo hinterlegt.",
logoUpload: "Logo hochladen",
logoRemove: "Logo entfernen",
logoUpdated: "Logo aktualisiert.",
logoRemoved: "Logo entfernt.",
faviconTitle: "Favicon",
faviconHint:
"Wird als Icon im Browser-Tab angezeigt. Ohne eigenes Favicon gilt das Standard-Icon der Anwendung.",
faviconNone: "Kein eigenes Favicon hinterlegt.",
faviconUpload: "Favicon hochladen",
faviconRemove: "Favicon entfernen",
faviconUpdated: "Favicon aktualisiert.",
faviconRemoved: "Favicon entfernt.",
invalidImage: "Bitte eine Bilddatei auswählen (PNG, JPG, SVG, WebP, ICO).",
colorsTitle: "Farbgebung",
colorsSave: "Farben speichern",
colorsReset: "Auf Standard zurücksetzen",
colorsSaved: "Farben gespeichert.",
colorPrimary: "Primärfarbe (Tabellenkopf)",
colorPrimaryDark: "Primärfarbe dunkel (Kopfleiste intern)",
colorAccent: "Akzentfarbe",
colorHeaderText: "Schriftfarbe im Tabellenkopf",
colorRowBg: "Zellen-Hintergrund",
colorRowBgAlt: "Zellen-Hintergrund (alternierend)",
colorAddBtn: "Button „Hinzufügen“",
colorDeleteBtn: "Button „Löschen“",
loadError: "Laden fehlgeschlagen: {message}",
saveError: "Speichern fehlgeschlagen: {message}",
uploadError: "Upload fehlgeschlagen: {message}",
confirmRemoveLogo: "Logo wirklich entfernen?",
confirmRemoveFavicon: "Favicon wirklich entfernen?",
},
passkeys: {
title: "Passkeys",
hint: "Mit einem Passkey (Fingerabdruck, Gesichtserkennung oder Sicherheitsschlüssel) kannst du dich künftig ohne Passwort anmelden. Passkeys gelten pro Domain – für den Live-Betrieb muss die Domain in der Supabase-Konfiguration hinterlegt sein.",
register: "Neuen Passkey registrieren",
empty: "Noch keine Passkeys registriert.",
createdOn: "– angelegt am {date}",
registered: "Passkey wurde erfolgreich registriert.",
registerError: "Registrierung fehlgeschlagen: {message}",
registerCancelled: "Die Passkey-Registrierung wurde abgebrochen.",
loadError: "Passkeys konnten nicht geladen werden: {message}",
deleteError: "Löschen fehlgeschlagen: {message}",
confirmDelete: "Diesen Passkey wirklich löschen?",
},
account: {
title: "Mein Konto",
personalTitle: "Persönliche Daten",
saveName: "Name speichern",
nameSaved: "Name gespeichert.",
nameRequired: "Bitte einen Namen angeben.",
emailTitle: "E-Mail-Adresse",
changeEmail: "E-Mail-Adresse ändern",
emailSent:
"Bestätigungs-E-Mails wurden verschickt. Die neue Adresse wird erst nach Bestätigung des Links aktiv.",
passwordTitle: "Passwort ändern",
currentPassword: "Aktuelles Passwort (zur Verifizierung)",
newPassword: "Neues Passwort",
newPasswordRepeat: "Neues Passwort wiederholen",
changePassword: "Passwort ändern",
passwordChanged: "Passwort geändert.",
passwordMismatch: "Die Passwörter stimmen nicht überein.",
passwordTooShort: "Das neue Passwort muss mindestens 8 Zeichen lang sein.",
passwordAuthTitle: "Anmeldung mit Passwort",
passwordAuthHint:
"Du kannst die Anmeldung mit E-Mail und Passwort für dein Konto deaktivieren und dich künftig ausschließlich per Passkey anmelden. Dafür muss mindestens ein Passkey registriert sein.",
passwordAuthCheckbox: "Anmeldung mit E-Mail und Passwort erlaubt",
passwordAuthEnabled: "Passwort-Anmeldung aktiviert.",
passwordAuthDisabled: "Passwort-Anmeldung deaktiviert.",
passwordAuthNeedsPasskey:
"Ohne registrierten Passkey kannst du die Passwort-Anmeldung nicht deaktivieren – sonst wärst du ausgesperrt. Registriere zuerst einen Passkey weiter unten.",
confirmDisablePasswordAuth:
"Anmeldung mit E-Mail und Passwort wirklich deaktivieren? Du kannst dich danach nur noch per Passkey anmelden.",
wrongCurrentPassword: "Das aktuelle Passwort ist falsch.",
emailAlreadyUsed: "Diese E-Mail-Adresse wird bereits verwendet.",
saveError: "Speichern fehlgeschlagen: {message}",
changeError: "Ändern fehlgeschlagen: {message}",
},
suggestions: {
formTitle: "Rezept vorschlagen",
formHint:
"Fehlt dir ein Gericht auf der Speisekarte? Schlage es hier vor – das Kantinen-Team sichtet deinen Vorschlag.",
nameLabel: "Gerichtname",
descriptionLabel: "Beschreibung / Zutaten (optional)",
submit: "Vorschlag senden",
submitting: "Wird gesendet…",
success: "Danke! Dein Vorschlag wurde übermittelt.",
duplicate: "Dieses Gericht gibt es bereits auf der Speisekarte.",
genericError: "Senden fehlgeschlagen: {message}",
nameRequired: "Bitte einen Namen angeben.",
backToMenu: "← Zur Speisekarte",
reviewTitle: "Rezeptvorschläge",
reviewEmpty: "Noch keine Vorschläge.",
accept: "Als Gericht übernehmen",
reject: "Ablehnen",
confirmReject: "Vorschlag „{name}“ wirklich ablehnen und löschen?",
accepted: "Als Gericht übernommen.",
rejected: "Vorschlag abgelehnt.",
loadError: "Laden fehlgeschlagen: {message}",
actionError: "Aktion fehlgeschlagen: {message}",
submittedOn: "eingereicht am {date}",
},
};
type Dict = typeof de;
const en: Dict = {
common: {
save: "Save",
cancel: "Cancel",
delete: "Delete",
edit: "Edit",
add: "Add",
back: "Back",
login: "Log in",
logout: "Log out",
loading: "Loading…",
you: "(you)",
name: "Name",
email: "Email address",
password: "Password",
role: "Role",
roleAdmin: "Administrator",
roleStaff: "Canteen staff",
},
weekdays: {
long: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
short: ["Mon", "Tue", "Wed", "Thu", "Fri"],
},
dateLocale: "en-GB",
header: {
canteen: "Canteen",
menuGroup: "Menu",
weekPlan: "Weekly plan",
dishes: "Dishes",
labels: "Allergens & Additives",
adminGroup: "Administration",
users: "Users",
design: "Design",
suggestions: "Suggestions",
viewMenu: "View menu",
myAccount: "My account",
},
login: {
title: "Sign in",
submit: "Log in",
or: "or",
passkeyButton: "Sign in with passkey",
backToMenu: "← Back to menu",
errorInvalidCredentials: "Email address or password is incorrect.",
errorEmailNotConfirmed: "This email address has not been confirmed yet.",
errorPasskeyCancelled: "Passkey sign-in was cancelled.",
errorGeneric: "Sign-in failed: {message}",
errorPasswordDisabled:
"Password sign-in is disabled for this account. Please sign in with a passkey.",
},
publicMenu: {
prevWeek: "← Previous week",
nextWeek: "Next week →",
prevWeekShort: "← Week",
nextWeekShort: "Week →",
today: "Today",
dayTab: "Day",
weekTab: "Week",
allergens: "Allergens",
additives: "Additives",
suggestDish: "Suggest a dish",
noEntry: "–",
},
weekEditor: {
title: "Weekly plan",
loading: "Loading weekly plan…",
selectDish: "– Select dish –",
addMeal: "Add meal",
newDishesHint: "Add new dishes under {link}.",
newDishesLinkLabel: "Dishes",
loadError: "Loading failed: {message}",
duplicateEntry: "This dish is already assigned on this day.",
saveError: "Saving failed: {message}",
deleteError: "Deleting failed: {message}",
},
dishManager: {
title: "Dishes",
newDish: "New dish",
editDish: "Edit dish",
nameLabel: "Name",
nameEnLabel: "Name (English)",
namePlLabel: "Name (Polish)",
autoTranslate: "Auto-translate",
translating: "Translating…",
translated: "Translated.",
translateUnavailable:
"Automatic translation is not set up. Please fill in English/Polish manually.",
priceLabel: "Price in €",
pricePlaceholder: "3.50",
searchPlaceholder: "Search dishes…",
selectAll: "Select all",
selectedCount: "{count} selected",
deleteSelected: "Delete selected",
deleteSelectedCount: "Delete selected ({count})",
empty: "No dishes yet.",
noResults: "No results for “{query}”.",
confirmDelete:
"Really delete “{name}”? The dish will also be removed from all weekly plans.",
confirmDeleteSelected:
"Really delete {count} dish(es)? They will also be removed from all weekly plans.",
invalidPrice: "Please enter a valid price, e.g. 3.50",
loadError: "Loading failed: {message}",
saveError: "Saving failed: {message}",
deleteError: "Deleting failed: {message}",
},
codeList: {
allergensTitle: "Allergens",
additivesTitle: "Additives",
allergenCodeLabel: "Code (e.g. A)",
additiveCodeLabel: "Code (e.g. 1)",
nameLabel: "Name",
empty: "No entries.",
confirmDelete: "Really delete “{code} – {name}”?",
duplicateCode: "The code “{code}” already exists.",
loadError: "Loading failed: {message}",
saveError: "Saving failed: {message}",
deleteError: "Deleting failed: {message}",
},
userManager: {
title: "Users",
newUser: "New user",
editUser: "Edit user",
newPasswordLabel: "New password (optional)",
repeatPasswordLabel: "Repeat password",
passwordMismatch: "The passwords do not match.",
confirmDelete:
"Really delete user “{email}”? Access will be permanently removed.",
loadError: "Loading failed: {message}",
},
designEditor: {
title: "Design",
logoTitle: "Logo",
logoHint: "The logo is displayed centered above the menu.",
logoNone: "No logo set.",
logoUpload: "Upload logo",
logoRemove: "Remove logo",
logoUpdated: "Logo updated.",
logoRemoved: "Logo removed.",
faviconTitle: "Favicon",
faviconHint:
"Shown as the icon in the browser tab. Without a custom favicon, the application's default icon is used.",
faviconNone: "No custom favicon set.",
faviconUpload: "Upload favicon",
faviconRemove: "Remove favicon",
faviconUpdated: "Favicon updated.",
faviconRemoved: "Favicon removed.",
invalidImage: "Please choose an image file (PNG, JPG, SVG, WebP, ICO).",
colorsTitle: "Colors",
colorsSave: "Save colors",
colorsReset: "Reset to default",
colorsSaved: "Colors saved.",
colorPrimary: "Primary color (table header)",
colorPrimaryDark: "Primary color, dark (internal top bar)",
colorAccent: "Accent color",
colorHeaderText: "Table header text color",
colorRowBg: "Cell background",
colorRowBgAlt: "Cell background (alternating)",
colorAddBtn: "“Add” button",
colorDeleteBtn: "“Delete” button",
loadError: "Loading failed: {message}",
saveError: "Saving failed: {message}",
uploadError: "Upload failed: {message}",
confirmRemoveLogo: "Really remove the logo?",
confirmRemoveFavicon: "Really remove the favicon?",
},
passkeys: {
title: "Passkeys",
hint: "With a passkey (fingerprint, face recognition, or a security key) you can sign in without a password in the future. Passkeys are bound to a domain – for production use, the domain must be configured in Supabase.",
register: "Register new passkey",
empty: "No passkeys registered yet.",
createdOn: "– created on {date}",
registered: "Passkey registered successfully.",
registerError: "Registration failed: {message}",
registerCancelled: "Passkey registration was cancelled.",
loadError: "Could not load passkeys: {message}",
deleteError: "Deleting failed: {message}",
confirmDelete: "Really delete this passkey?",
},
account: {
title: "My account",
personalTitle: "Personal details",
saveName: "Save name",
nameSaved: "Name saved.",
nameRequired: "Please enter a name.",
emailTitle: "Email address",
changeEmail: "Change email address",
emailSent:
"Confirmation emails have been sent. The new address becomes active only after confirming the link.",
passwordTitle: "Change password",
currentPassword: "Current password (for verification)",
newPassword: "New password",
newPasswordRepeat: "Repeat new password",
changePassword: "Change password",
passwordChanged: "Password changed.",
passwordMismatch: "The passwords do not match.",
passwordTooShort: "The new password must be at least 8 characters long.",
passwordAuthTitle: "Password sign-in",
passwordAuthHint:
"You can disable email and password sign-in for your account and sign in exclusively with a passkey in the future. At least one passkey must be registered for this.",
passwordAuthCheckbox: "Email and password sign-in allowed",
passwordAuthEnabled: "Password sign-in enabled.",
passwordAuthDisabled: "Password sign-in disabled.",
passwordAuthNeedsPasskey:
"You can't disable password sign-in without a registered passkey – you would lock yourself out. Register a passkey below first.",
confirmDisablePasswordAuth:
"Really disable email and password sign-in? Afterwards you can only sign in with a passkey.",
wrongCurrentPassword: "The current password is incorrect.",
emailAlreadyUsed: "This email address is already in use.",
saveError: "Saving failed: {message}",
changeError: "Change failed: {message}",
},
suggestions: {
formTitle: "Suggest a dish",
formHint:
"Missing a dish on the menu? Suggest it here – the canteen team will review your suggestion.",
nameLabel: "Dish name",
descriptionLabel: "Description / ingredients (optional)",
submit: "Submit suggestion",
submitting: "Submitting…",
success: "Thanks! Your suggestion has been submitted.",
duplicate: "This dish already exists on the menu.",
genericError: "Submission failed: {message}",
nameRequired: "Please enter a name.",
backToMenu: "← Back to menu",
reviewTitle: "Dish suggestions",
reviewEmpty: "No suggestions yet.",
accept: "Add as dish",
reject: "Reject",
confirmReject: "Really reject and delete the suggestion “{name}”?",
accepted: "Added as dish.",
rejected: "Suggestion rejected.",
loadError: "Loading failed: {message}",
actionError: "Action failed: {message}",
submittedOn: "submitted on {date}",
},
};
const pl: Dict = {
common: {
save: "Zapisz",
cancel: "Anuluj",
delete: "Usuń",
edit: "Edytuj",
add: "Dodaj",
back: "Wstecz",
login: "Zaloguj się",
logout: "Wyloguj się",
loading: "Wczytywanie…",
you: "(ty)",
name: "Nazwa",
email: "Adres e-mail",
password: "Hasło",
role: "Rola",
roleAdmin: "Administrator",
roleStaff: "Personel stołówki",
},
weekdays: {
long: ["Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek"],
short: ["Pon", "Wto", "Śro", "Czw", "Pią"],
},
dateLocale: "pl-PL",
header: {
canteen: "Stołówka",
menuGroup: "Jadłospis",
weekPlan: "Plan tygodnia",
dishes: "Dania",
labels: "Alergeny i dodatki",
adminGroup: "Administracja",
users: "Użytkownicy",
design: "Wygląd",
suggestions: "Propozycje",
viewMenu: "Zobacz jadłospis",
myAccount: "Moje konto",
},
login: {
title: "Logowanie",
submit: "Zaloguj się",
or: "lub",
passkeyButton: "Zaloguj się kluczem dostępu",
backToMenu: "← Powrót do jadłospisu",
errorInvalidCredentials: "Nieprawidłowy adres e-mail lub hasło.",
errorEmailNotConfirmed: "Adres e-mail nie został jeszcze potwierdzony.",
errorPasskeyCancelled: "Logowanie kluczem dostępu zostało anulowane.",
errorGeneric: "Logowanie nie powiodło się: {message}",
errorPasswordDisabled:
"Logowanie hasłem jest wyłączone dla tego konta. Zaloguj się kluczem dostępu.",
},
publicMenu: {
prevWeek: "← Poprzedni tydzień",
nextWeek: "Następny tydzień →",
prevWeekShort: "← Tydzień",
nextWeekShort: "Tydzień →",
today: "Dziś",
dayTab: "Dzień",
weekTab: "Tydzień",
allergens: "Alergeny",
additives: "Dodatki",
suggestDish: "Zaproponuj danie",
noEntry: "–",
},
weekEditor: {
title: "Plan tygodnia",
loading: "Wczytywanie planu tygodnia…",
selectDish: "– Wybierz danie –",
addMeal: "Dodaj posiłek",
newDishesHint: "Nowe dania możesz dodać w zakładce {link}.",
newDishesLinkLabel: "Dania",
loadError: "Wczytywanie nie powiodło się: {message}",
duplicateEntry: "To danie jest już przypisane na ten dzień.",
saveError: "Zapisywanie nie powiodło się: {message}",
deleteError: "Usuwanie nie powiodło się: {message}",
},
dishManager: {
title: "Dania",
newDish: "Nowe danie",
editDish: "Edytuj danie",
nameLabel: "Nazwa",
nameEnLabel: "Nazwa (angielski)",
namePlLabel: "Nazwa (polski)",
autoTranslate: "Przetłumacz automatycznie",
translating: "Tłumaczenie…",
translated: "Przetłumaczono.",
translateUnavailable:
"Automatyczne tłumaczenie nie jest skonfigurowane. Wypełnij angielski/polski ręcznie.",
priceLabel: "Cena w €",
pricePlaceholder: "3,50",
searchPlaceholder: "Szukaj dania…",
selectAll: "Zaznacz wszystko",
selectedCount: "Zaznaczono: {count}",
deleteSelected: "Usuń zaznaczone",
deleteSelectedCount: "Usuń zaznaczone ({count})",
empty: "Nie dodano jeszcze żadnych dań.",
noResults: "Brak wyników dla „{query}”.",
confirmDelete:
"Czy na pewno usunąć „{name}”? Danie zostanie również usunięte ze wszystkich planów tygodniowych.",
confirmDeleteSelected:
"Czy na pewno usunąć {count} danie/dania? Zostaną również usunięte ze wszystkich planów tygodniowych.",
invalidPrice: "Podaj prawidłową cenę, np. 3,50",
loadError: "Wczytywanie nie powiodło się: {message}",
saveError: "Zapisywanie nie powiodło się: {message}",
deleteError: "Usuwanie nie powiodło się: {message}",
},
codeList: {
allergensTitle: "Alergeny",
additivesTitle: "Dodatki",
allergenCodeLabel: "Kod (np. A)",
additiveCodeLabel: "Kod (np. 1)",
nameLabel: "Nazwa",
empty: "Brak wpisów.",
confirmDelete: "Czy na pewno usunąć „{code} – {name}”?",
duplicateCode: "Kod „{code}” już istnieje.",
loadError: "Wczytywanie nie powiodło się: {message}",
saveError: "Zapisywanie nie powiodło się: {message}",
deleteError: "Usuwanie nie powiodło się: {message}",
},
userManager: {
title: "Użytkownicy",
newUser: "Nowy użytkownik",
editUser: "Edytuj użytkownika",
newPasswordLabel: "Nowe hasło (opcjonalnie)",
repeatPasswordLabel: "Powtórz hasło",
passwordMismatch: "Hasła nie są zgodne.",
confirmDelete:
"Czy na pewno usunąć użytkownika „{email}”? Dostęp zostanie trwale usunięty.",
loadError: "Wczytywanie nie powiodło się: {message}",
},
designEditor: {
title: "Wygląd",
logoTitle: "Logo",
logoHint: "Logo jest wyświetlane wyśrodkowane nad jadłospisem.",
logoNone: "Nie ustawiono logo.",
logoUpload: "Prześlij logo",
logoRemove: "Usuń logo",
logoUpdated: "Logo zaktualizowane.",
logoRemoved: "Logo usunięte.",
faviconTitle: "Favicon",
faviconHint:
"Wyświetlane jako ikona na karcie przeglądarki. Bez własnego favicona używana jest domyślna ikona aplikacji.",
faviconNone: "Nie ustawiono własnego favicona.",
faviconUpload: "Prześlij favicon",
faviconRemove: "Usuń favicon",
faviconUpdated: "Favicon zaktualizowany.",
faviconRemoved: "Favicon usunięty.",
invalidImage: "Wybierz plik graficzny (PNG, JPG, SVG, WebP, ICO).",
colorsTitle: "Kolorystyka",
colorsSave: "Zapisz kolory",
colorsReset: "Przywróć domyślne",
colorsSaved: "Kolory zapisane.",
colorPrimary: "Kolor główny (nagłówek tabeli)",
colorPrimaryDark: "Kolor główny, ciemny (górny pasek wewnętrzny)",
colorAccent: "Kolor akcentu",
colorHeaderText: "Kolor tekstu w nagłówku tabeli",
colorRowBg: "Tło komórek",
colorRowBgAlt: "Tło komórek (naprzemienne)",
colorAddBtn: "Przycisk „Dodaj”",
colorDeleteBtn: "Przycisk „Usuń”",
loadError: "Wczytywanie nie powiodło się: {message}",
saveError: "Zapisywanie nie powiodło się: {message}",
uploadError: "Przesyłanie nie powiodło się: {message}",
confirmRemoveLogo: "Czy na pewno usunąć logo?",
confirmRemoveFavicon: "Czy na pewno usunąć favicon?",
},
passkeys: {
title: "Klucze dostępu",
hint: "Dzięki kluczowi dostępu (odcisk palca, rozpoznawanie twarzy lub klucz bezpieczeństwa) możesz w przyszłości logować się bez hasła. Klucze dostępu są przypisane do domeny – w środowisku produkcyjnym domena musi być skonfigurowana w Supabase.",
register: "Zarejestruj nowy klucz dostępu",
empty: "Nie zarejestrowano jeszcze żadnych kluczy dostępu.",
createdOn: "– utworzono {date}",
registered: "Klucz dostępu został pomyślnie zarejestrowany.",
registerError: "Rejestracja nie powiodła się: {message}",
registerCancelled: "Rejestracja klucza dostępu została anulowana.",
loadError: "Nie udało się wczytać kluczy dostępu: {message}",
deleteError: "Usuwanie nie powiodło się: {message}",
confirmDelete: "Czy na pewno usunąć ten klucz dostępu?",
},
account: {
title: "Moje konto",
personalTitle: "Dane osobowe",
saveName: "Zapisz nazwę",
nameSaved: "Nazwa zapisana.",
nameRequired: "Podaj nazwę.",
emailTitle: "Adres e-mail",
changeEmail: "Zmień adres e-mail",
emailSent:
"Wysłano e-maile potwierdzające. Nowy adres zacznie obowiązywać dopiero po potwierdzeniu linku.",
passwordTitle: "Zmień hasło",
currentPassword: "Obecne hasło (do weryfikacji)",
newPassword: "Nowe hasło",
newPasswordRepeat: "Powtórz nowe hasło",
changePassword: "Zmień hasło",
passwordChanged: "Hasło zmienione.",
passwordMismatch: "Hasła nie są zgodne.",
passwordTooShort: "Nowe hasło musi mieć co najmniej 8 znaków.",
passwordAuthTitle: "Logowanie hasłem",
passwordAuthHint:
"Możesz wyłączyć logowanie e-mailem i hasłem dla swojego konta i logować się w przyszłości wyłącznie kluczem dostępu. Wymaga to zarejestrowania co najmniej jednego klucza dostępu.",
passwordAuthCheckbox: "Logowanie e-mailem i hasłem dozwolone",
passwordAuthEnabled: "Logowanie hasłem włączone.",
passwordAuthDisabled: "Logowanie hasłem wyłączone.",
passwordAuthNeedsPasskey:
"Bez zarejestrowanego klucza dostępu nie możesz wyłączyć logowania hasłem – stracisz dostęp do konta. Najpierw zarejestruj klucz dostępu poniżej.",
confirmDisablePasswordAuth:
"Czy na pewno wyłączyć logowanie e-mailem i hasłem? Potem będziesz mógł/mogła zalogować się tylko kluczem dostępu.",
wrongCurrentPassword: "Obecne hasło jest nieprawidłowe.",
emailAlreadyUsed: "Ten adres e-mail jest już używany.",
saveError: "Zapisywanie nie powiodło się: {message}",
changeError: "Zmiana nie powiodła się: {message}",
},
suggestions: {
formTitle: "Zaproponuj danie",
formHint:
"Brakuje dania w jadłospisie? Zaproponuj je tutaj – zespół stołówki sprawdzi Twoją propozycję.",
nameLabel: "Nazwa dania",
descriptionLabel: "Opis / składniki (opcjonalnie)",
submit: "Wyślij propozycję",
submitting: "Wysyłanie…",
success: "Dziękujemy! Twoja propozycja została przesłana.",
duplicate: "To danie już istnieje w jadłospisie.",
genericError: "Wysyłanie nie powiodło się: {message}",
nameRequired: "Podaj nazwę.",
backToMenu: "← Powrót do jadłospisu",
reviewTitle: "Propozycje dań",
reviewEmpty: "Brak propozycji.",
accept: "Dodaj jako danie",
reject: "Odrzuć",
confirmReject: "Czy na pewno odrzucić i usunąć propozycję „{name}”?",
accepted: "Dodano jako danie.",
rejected: "Propozycja odrzucona.",
loadError: "Wczytywanie nie powiodło się: {message}",
actionError: "Czynność nie powiodła się: {message}",
submittedOn: "przesłano {date}",
},
};
export type { Dict };
export const dictionaries = { de, en, pl };
+16
View File
@@ -0,0 +1,16 @@
// Nur für Server-Komponenten/Routen importieren (next/headers)!
// Client-Komponenten müssen stattdessen "@/lib/i18n/shared" verwenden,
// sonst landet next/headers im Client-Bundle.
import "server-only";
import { cookies } from "next/headers";
import { DEFAULT_LOCALE, isLocale, type Locale } from "./locales";
export * from "./shared";
/** Aktuelle Sprache aus dem Cookie lesen (Server-Komponenten). */
export async function getLocale(): Promise<Locale> {
const store = await cookies();
const value = store.get("locale")?.value;
return isLocale(value) ? value : DEFAULT_LOCALE;
}
+13
View File
@@ -0,0 +1,13 @@
export const LOCALES = ["de", "en", "pl"] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = "de";
export const LOCALE_LABELS: Record<Locale, string> = {
de: "Deutsch",
en: "English",
pl: "Polski",
};
export function isLocale(value: string | undefined | null): value is Locale {
return !!value && (LOCALES as readonly string[]).includes(value);
}
+26
View File
@@ -0,0 +1,26 @@
// Client-sicherer Teil von lib/i18n: keine Abhängigkeit auf next/headers,
// darf daher auch von "use client"-Komponenten importiert werden.
import { dictionaries, type Dict } from "./dictionaries";
import type { Locale } from "./locales";
export { LOCALES, LOCALE_LABELS, DEFAULT_LOCALE, isLocale } from "./locales";
export type { Locale } from "./locales";
export type { Dict } from "./dictionaries";
export const LOCALE_COOKIE = "locale";
export function getDictionary(locale: Locale): Dict {
return dictionaries[locale];
}
/** Platzhalter der Form "{name}" durch vars.name ersetzen. */
export function formatMessage(
template: string,
vars?: Record<string, string | number>
): string {
if (!vars) return template;
return template.replace(/\{(\w+)\}/g, (match, key) =>
key in vars ? String(vars[key]) : match
);
}