Files
Speisekarte/db/migrations/0005_seed_ui_translations.sql
florian.wildeandClaude Sonnet 5 236f8c9490 feat: Dynamisches Sprachpaket-System für die Bedienoberfläche
Teil 1 des "mehrsprachig variabel"-Umbaus (Speiseplan-Inhalte folgen
in einer späteren Version). Sprachen sind jetzt zur Laufzeit über eine
neue Admin-Seite "Sprachen" verwaltbar statt fest im Code zu stehen:

- Neue Tabellen languages/ui_translations (Migration 0004), bestehende
  Englisch-/Polnisch-Texte per generiertem Seed automatisch übernommen
  (Migration 0005) - keine manuelle Nacharbeit, nichts geht verloren.
- lib/i18n/flatten.ts: verschachtelte Dict-Objekte (inkl. Arrays wie
  weekdays.long) <-> flache Schlüssel-Wert-Paare, Basis für Vorlage-
  Export/-Import.
- lib/i18n/template.ts: Vollständigkeits-/Veraltet-Berechnung (Hash
  über die DE-Schlüsselliste) und Import-Validierung (Platzhalter wie
  {message} müssen erhalten bleiben, unbekannte Schlüssel werden
  ignoriert statt Fehler zu werfen).
- getDictionary()/getLocale() (lib/i18n/index.ts) jetzt asynchron und
  DB-gestützt für Nicht-Basis-Sprachen, mit Fallback auf Deutsch für
  fehlende Schlüssel. Locale-Typ bewusst von festem TypeScript-Tupel
  auf string generalisiert (Kompromiss: Compiler prüft Vollständigkeit
  nicht mehr automatisch, das übernimmt jetzt die Veraltet-Erkennung
  zur Laufzeit).
- LanguageSwitcher/LocaleProvider lesen aktive Sprachen jetzt aus dem
  Context statt aus einer festen Konstante; blendet sich bei nur einer
  aktiven Sprache automatisch aus.
- Neue Admin-Routen app/api/admin/languages/* (Liste inkl.
  Vollständigkeit, Anlegen, Aktivieren/Deaktivieren mit
  "mindestens eine muss aktiv bleiben"-Schutz, Löschen außer
  Basis-Sprache, Vorlage-Download, Datei-Import) + LanguageManager.tsx.

Vollständig gegen echten DB-Zustand getestet: Vorlage-Export/-Import
inkl. Platzhalter-Validierung, neue Sprache anlegen, auf eine Sprache
reduzieren (Umschalter verschwindet, "letzte Sprache"-Schutz greift),
Veraltet-Erkennung nach simuliertem neuen Schlüssel, Cascade-Delete.

Version 1.10.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:34:51 +02:00

575 lines
37 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Einmaliger Seed: bisherige fest im Code hinterlegte Englisch-/Polnisch-
-- Übersetzungen (lib/i18n/dictionaries.ts) als Sprachpakete in
-- ui_translations überführen, damit nach dem Umbau auf das dynamische
-- Sprachpaket-System (siehe 0004_languages.sql) nichts verloren geht.
insert into ui_translations (language_code, key, value) values
('en', 'common.save', 'Save'),
('en', 'common.cancel', 'Cancel'),
('en', 'common.delete', 'Delete'),
('en', 'common.edit', 'Edit'),
('en', 'common.add', 'Add'),
('en', 'common.back', 'Back'),
('en', 'common.login', 'Log in'),
('en', 'common.logout', 'Log out'),
('en', 'common.loading', 'Loading…'),
('en', 'common.you', '(you)'),
('en', 'common.name', 'Name'),
('en', 'common.email', 'Email address'),
('en', 'common.password', 'Password'),
('en', 'common.role', 'Role'),
('en', 'common.roleAdmin', 'Administrator'),
('en', 'common.roleStaff', 'Canteen staff'),
('en', 'weekdays.long.0', 'Monday'),
('en', 'weekdays.long.1', 'Tuesday'),
('en', 'weekdays.long.2', 'Wednesday'),
('en', 'weekdays.long.3', 'Thursday'),
('en', 'weekdays.long.4', 'Friday'),
('en', 'weekdays.short.0', 'Mon'),
('en', 'weekdays.short.1', 'Tue'),
('en', 'weekdays.short.2', 'Wed'),
('en', 'weekdays.short.3', 'Thu'),
('en', 'weekdays.short.4', 'Fri'),
('en', 'dateLocale', 'en-GB'),
('en', 'header.canteen', 'Canteen'),
('en', 'header.menuGroup', 'Menu'),
('en', 'header.weekPlan', 'Weekly plan'),
('en', 'header.sections', 'Meal times'),
('en', 'header.dishes', 'Dishes'),
('en', 'header.labels', 'Allergens & Additives'),
('en', 'header.adminGroup', 'Administration'),
('en', 'header.users', 'Users'),
('en', 'header.design', 'Design'),
('en', 'header.languages', 'Languages'),
('en', 'header.suggestions', 'Suggestions'),
('en', 'header.openSuggestions', '{count} open dish suggestions'),
('en', 'header.viewMenu', 'View menu'),
('en', 'header.myAccount', 'My account'),
('en', 'header.menuToggle', 'Open/close menu'),
('en', 'login.title', 'Sign in'),
('en', 'login.submit', 'Log in'),
('en', 'login.or', 'or'),
('en', 'login.passkeyButton', 'Sign in with passkey'),
('en', 'login.ssoButton', 'Sign in with Single Sign-On'),
('en', 'login.backToMenu', '← Back to menu'),
('en', 'login.errorInvalidCredentials', 'Email address or password is incorrect.'),
('en', 'login.errorPasskeyCancelled', 'Passkey sign-in was cancelled.'),
('en', 'login.errorGeneric', 'Sign-in failed: {message}'),
('en', 'login.errorPasswordDisabled', 'Password sign-in is disabled for this account. Please sign in with a passkey.'),
('en', 'login.errorOidcInvalidState', 'The SSO sign-in expired or was invalid. Please try again.'),
('en', 'login.errorOidcUnknownUser', 'No account exists for this email address. Please contact your administrator.'),
('en', 'login.errorOidcFailed', 'SSO sign-in failed. Please try again.'),
('en', 'publicMenu.prevWeek', '← Previous week'),
('en', 'publicMenu.nextWeek', 'Next week →'),
('en', 'publicMenu.prevWeekShort', '← Week'),
('en', 'publicMenu.nextWeekShort', 'Week →'),
('en', 'publicMenu.today', 'Today'),
('en', 'publicMenu.dayTab', 'Day'),
('en', 'publicMenu.weekTab', 'Week'),
('en', 'publicMenu.allergens', 'Allergens'),
('en', 'publicMenu.additives', 'Additives'),
('en', 'publicMenu.labelingNoticeTitle', 'Allergens & additives'),
('en', 'publicMenu.suggestDish', 'Suggest a dish'),
('en', 'publicMenu.noEntry', '–'),
('en', 'publicMenu.rssFeed', 'Subscribe to RSS feed'),
('en', 'weekEditor.title', 'Weekly plan'),
('en', 'weekEditor.loading', 'Loading weekly plan…'),
('en', 'weekEditor.changeDish', 'Choose a different dish'),
('en', 'weekEditor.addMeal', 'Add meal'),
('en', 'weekEditor.newDishesHint', 'Add new dishes under {link}.'),
('en', 'weekEditor.newDishesLinkLabel', 'Dishes'),
('en', 'weekEditor.loadError', 'Loading failed: {message}'),
('en', 'weekEditor.duplicateEntry', 'This dish is already assigned on this day.'),
('en', 'weekEditor.saveError', 'Saving failed: {message}'),
('en', 'weekEditor.deleteError', 'Deleting failed: {message}'),
('en', 'dishManager.title', 'Dishes'),
('en', 'dishManager.newDish', 'New dish'),
('en', 'dishManager.editDish', 'Edit dish'),
('en', 'dishManager.nameLabel', 'Name'),
('en', 'dishManager.nameEnLabel', 'Name (English)'),
('en', 'dishManager.namePlLabel', 'Name (Polish)'),
('en', 'dishManager.autoTranslate', 'Auto-translate'),
('en', 'dishManager.translating', 'Translating…'),
('en', 'dishManager.translated', 'Translated.'),
('en', 'dishManager.priceLabel', 'Price in €'),
('en', 'dishManager.pricePlaceholder', '3.50'),
('en', 'dishManager.searchPlaceholder', 'Search dishes…'),
('en', 'dishManager.selectAll', 'Select all'),
('en', 'dishManager.selectedCount', '{count} selected'),
('en', 'dishManager.deleteSelected', 'Delete selected'),
('en', 'dishManager.deleteSelectedCount', 'Delete selected ({count})'),
('en', 'dishManager.empty', 'No dishes yet.'),
('en', 'dishManager.noResults', 'No results for “{query}”.'),
('en', 'dishManager.confirmDelete', 'Really delete “{name}”? The dish will also be removed from all weekly plans.'),
('en', 'dishManager.confirmDeleteSelected', 'Really delete {count} dish(es)? They will also be removed from all weekly plans.'),
('en', 'dishManager.invalidPrice', 'Please enter a valid price, e.g. 3.50'),
('en', 'dishManager.loadError', 'Loading failed: {message}'),
('en', 'dishManager.saveError', 'Saving failed: {message}'),
('en', 'dishManager.deleteError', 'Deleting failed: {message}'),
('en', 'dishManager.exportButton', 'Export'),
('en', 'dishManager.importButton', 'Import'),
('en', 'dishManager.exportError', 'Export failed: {message}'),
('en', 'dishManager.importError', 'Import failed: {message}'),
('en', 'dishManager.importSuccess', '{created} created, {updated} updated.'),
('en', 'dishManager.importSkipped', '{count} row(s) skipped: {reasons}'),
('en', 'dishManager.importSkippedRow', 'Row {row}: {reason}'),
('en', 'dishManager.labelingDisabledHint', 'Allergens/additives are currently disabled (notice mode active, see "Allergens & additives").'),
('en', 'codeList.allergensTitle', 'Allergens'),
('en', 'codeList.additivesTitle', 'Additives'),
('en', 'codeList.allergenCodeLabel', 'Code (e.g. A)'),
('en', 'codeList.additiveCodeLabel', 'Code (e.g. 1)'),
('en', 'codeList.nameLabel', 'Name'),
('en', 'codeList.empty', 'No entries.'),
('en', 'codeList.confirmDelete', 'Really delete “{code} – {name}”?'),
('en', 'codeList.duplicateCode', 'The code “{code}” already exists.'),
('en', 'codeList.loadError', 'Loading failed: {message}'),
('en', 'codeList.saveError', 'Saving failed: {message}'),
('en', 'codeList.deleteError', 'Deleting failed: {message}'),
('en', 'codeList.exportButton', 'Export'),
('en', 'codeList.importButton', 'Import'),
('en', 'codeList.exportError', 'Export failed: {message}'),
('en', 'codeList.importError', 'Import failed: {message}'),
('en', 'codeList.importSuccess', '{created} created, {updated} updated.'),
('en', 'codeList.importSkipped', '{count} row(s) skipped: {reasons}'),
('en', 'codeList.importSkippedRow', 'Row {row}: {reason}'),
('en', 'sectionManager.title', 'Meal times'),
('en', 'sectionManager.newSection', 'New meal time'),
('en', 'sectionManager.editSection', 'Edit meal time'),
('en', 'sectionManager.nameLabel', 'Name'),
('en', 'sectionManager.nameEnLabel', 'Name (English)'),
('en', 'sectionManager.namePlLabel', 'Name (Polish)'),
('en', 'sectionManager.timeFrom', 'From'),
('en', 'sectionManager.timeTo', 'To'),
('en', 'sectionManager.moveUp', 'Move up'),
('en', 'sectionManager.moveDown', 'Move down'),
('en', 'sectionManager.empty', 'No meal times yet.'),
('en', 'sectionManager.confirmDelete', 'Really delete “{name}”? Assigned dishes will also be removed from all weekly plans.'),
('en', 'sectionManager.loadError', 'Loading failed: {message}'),
('en', 'sectionManager.saveError', 'Saving failed: {message}'),
('en', 'sectionManager.deleteError', 'Deleting failed: {message}'),
('en', 'userManager.title', 'Users'),
('en', 'userManager.newUser', 'New user'),
('en', 'userManager.editUser', 'Edit user'),
('en', 'userManager.newPasswordLabel', 'New password (optional)'),
('en', 'userManager.repeatPasswordLabel', 'Repeat password'),
('en', 'userManager.passwordMismatch', 'The passwords do not match.'),
('en', 'userManager.confirmDelete', 'Really delete user “{email}”? Access will be permanently removed.'),
('en', 'userManager.loadError', 'Loading failed: {message}'),
('en', 'designEditor.title', 'Design'),
('en', 'designEditor.logoTitle', 'Logo'),
('en', 'designEditor.logoHint', 'The logo is displayed centered above the menu.'),
('en', 'designEditor.logoNone', 'No logo set.'),
('en', 'designEditor.logoUpload', 'Upload logo'),
('en', 'designEditor.logoRemove', 'Remove logo'),
('en', 'designEditor.logoUpdated', 'Logo updated.'),
('en', 'designEditor.logoRemoved', 'Logo removed.'),
('en', 'designEditor.faviconTitle', 'Favicon'),
('en', 'designEditor.faviconHint', 'Shown as the icon in the browser tab. Without a custom favicon, the application''s default icon is used.'),
('en', 'designEditor.faviconNone', 'No custom favicon set.'),
('en', 'designEditor.faviconUpload', 'Upload favicon'),
('en', 'designEditor.faviconRemove', 'Remove favicon'),
('en', 'designEditor.faviconUpdated', 'Favicon updated.'),
('en', 'designEditor.faviconRemoved', 'Favicon removed.'),
('en', 'designEditor.invalidImage', 'Please choose an image file (PNG, JPG, SVG, WebP, ICO).'),
('en', 'designEditor.colorsTitle', 'Colors'),
('en', 'designEditor.colorsSave', 'Save colors'),
('en', 'designEditor.colorsReset', 'Reset to default'),
('en', 'designEditor.colorsSaved', 'Colors saved.'),
('en', 'designEditor.colorPrimary', 'Primary color (table header)'),
('en', 'designEditor.colorPrimaryDark', 'Primary color, dark (internal top bar)'),
('en', 'designEditor.colorAccent', 'Accent color'),
('en', 'designEditor.colorHeaderText', 'Table header text color'),
('en', 'designEditor.colorRowBg', 'Cell background'),
('en', 'designEditor.colorRowBgAlt', 'Cell background (alternating)'),
('en', 'designEditor.colorAddBtn', '“Add” button'),
('en', 'designEditor.colorDeleteBtn', '“Delete” button'),
('en', 'designEditor.loadError', 'Loading failed: {message}'),
('en', 'designEditor.saveError', 'Saving failed: {message}'),
('en', 'designEditor.uploadError', 'Upload failed: {message}'),
('en', 'designEditor.confirmRemoveLogo', 'Really remove the logo?'),
('en', 'designEditor.confirmRemoveFavicon', 'Really remove the favicon?'),
('en', 'labelingNotice.title', 'Allergen & additive display'),
('en', 'labelingNotice.hint', 'By default every guest sees a detailed allergen/additive selection per dish. Alternatively, only a notice text can be shown instead – the per-dish selection is then also hidden in the dish manager.'),
('en', 'labelingNotice.modeDetailed', 'Show detailed selection per dish'),
('en', 'labelingNotice.modeNotice', 'Show only notice text'),
('en', 'labelingNotice.noticeDeLabel', 'Notice text (German)'),
('en', 'labelingNotice.noticeEnLabel', 'Notice text (English)'),
('en', 'labelingNotice.noticePlLabel', 'Notice text (Polish)'),
('en', 'labelingNotice.save', 'Save'),
('en', 'labelingNotice.saved', 'Saved.'),
('en', 'labelingNotice.loadError', 'Loading failed: {message}'),
('en', 'labelingNotice.saveError', 'Saving failed: {message}'),
('en', 'passkeys.title', 'Passkeys'),
('en', '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 – after a domain change they need to be registered again.'),
('en', 'passkeys.register', 'Register new passkey'),
('en', 'passkeys.empty', 'No passkeys registered yet.'),
('en', 'passkeys.createdOn', '– created on {date}'),
('en', 'passkeys.registered', 'Passkey registered successfully.'),
('en', 'passkeys.registerError', 'Registration failed: {message}'),
('en', 'passkeys.registerCancelled', 'Passkey registration was cancelled.'),
('en', 'passkeys.loadError', 'Could not load passkeys: {message}'),
('en', 'passkeys.deleteError', 'Deleting failed: {message}'),
('en', 'passkeys.confirmDelete', 'Really delete this passkey?'),
('en', 'account.title', 'My account'),
('en', 'account.personalTitle', 'Personal details'),
('en', 'account.saveName', 'Save name'),
('en', 'account.nameSaved', 'Name saved.'),
('en', 'account.nameRequired', 'Please enter a name.'),
('en', 'account.emailTitle', 'Email address'),
('en', 'account.changeEmail', 'Change email address'),
('en', 'account.emailChanged', 'Email address changed.'),
('en', 'account.passwordTitle', 'Change password'),
('en', 'account.currentPassword', 'Current password (for verification)'),
('en', 'account.newPassword', 'New password'),
('en', 'account.newPasswordRepeat', 'Repeat new password'),
('en', 'account.changePassword', 'Change password'),
('en', 'account.passwordChanged', 'Password changed.'),
('en', 'account.passwordMismatch', 'The passwords do not match.'),
('en', 'account.passwordTooShort', 'The new password must be at least 8 characters long.'),
('en', 'account.passwordAuthTitle', 'Password sign-in'),
('en', 'account.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.'),
('en', 'account.passwordAuthCheckbox', 'Email and password sign-in allowed'),
('en', 'account.passwordAuthEnabled', 'Password sign-in enabled.'),
('en', 'account.passwordAuthDisabled', 'Password sign-in disabled.'),
('en', 'account.passwordAuthNeedsPasskey', 'You can''t disable password sign-in without a registered passkey – you would lock yourself out. Register a passkey below first.'),
('en', 'account.confirmDisablePasswordAuth', 'Really disable email and password sign-in? Afterwards you can only sign in with a passkey.'),
('en', 'account.wrongCurrentPassword', 'The current password is incorrect.'),
('en', 'account.emailAlreadyUsed', 'This email address is already in use.'),
('en', 'account.saveError', 'Saving failed: {message}'),
('en', 'account.changeError', 'Change failed: {message}'),
('en', 'suggestions.formTitle', 'Suggest a dish'),
('en', 'suggestions.formHint', 'Missing a dish on the menu? Suggest it here – the canteen team will review your suggestion.'),
('en', 'suggestions.nameLabel', 'Dish name'),
('en', 'suggestions.descriptionLabel', 'Description / ingredients (optional)'),
('en', 'suggestions.submit', 'Submit suggestion'),
('en', 'suggestions.submitting', 'Submitting…'),
('en', 'suggestions.success', 'Thanks! Your suggestion has been submitted.'),
('en', 'suggestions.duplicate', 'This dish already exists on the menu.'),
('en', 'suggestions.genericError', 'Submission failed: {message}'),
('en', 'suggestions.nameRequired', 'Please enter a name.'),
('en', 'suggestions.backToMenu', '← Back to menu'),
('en', 'suggestions.reviewTitle', 'Dish suggestions'),
('en', 'suggestions.reviewEmpty', 'No suggestions yet.'),
('en', 'suggestions.accept', 'Add as dish'),
('en', 'suggestions.reject', 'Reject'),
('en', 'suggestions.confirmReject', 'Really reject and delete the suggestion “{name}”?'),
('en', 'suggestions.accepted', 'Added as dish.'),
('en', 'suggestions.rejected', 'Suggestion rejected.'),
('en', 'suggestions.loadError', 'Loading failed: {message}'),
('en', 'suggestions.actionError', 'Action failed: {message}'),
('en', 'suggestions.submittedOn', 'submitted on {date}'),
('en', 'languageManager.title', 'Languages'),
('en', 'languageManager.hint', 'German is the fixed base language and always complete. Additional languages are imported as a translation file and automatically filled in with German wherever a term is missing.'),
('en', 'languageManager.codeColumn', 'Code'),
('en', 'languageManager.labelColumn', 'Display name'),
('en', 'languageManager.activeColumn', 'Active'),
('en', 'languageManager.completenessColumn', 'Completeness'),
('en', 'languageManager.baseBadge', 'Base language'),
('en', 'languageManager.complete', 'Complete'),
('en', 'languageManager.completeness', '{percent}% translated'),
('en', 'languageManager.outdated', '{count} new term(s) since last import'),
('en', 'languageManager.downloadTemplate', 'Download template'),
('en', 'languageManager.uploadTranslation', 'Upload translation'),
('en', 'languageManager.newLanguageTitle', 'Add new language'),
('en', 'languageManager.newLanguageCodeLabel', 'Language code (e.g. fr)'),
('en', 'languageManager.newLanguageLabelLabel', 'Display name (e.g. Français)'),
('en', 'languageManager.create', 'Create'),
('en', 'languageManager.delete', 'Delete'),
('en', 'languageManager.confirmDelete', 'Really delete “{label}”? All translations for this language will be lost.'),
('en', 'languageManager.cannotDeleteBase', 'The base language cannot be deleted.'),
('en', 'languageManager.cannotDeactivateLast', 'At least one language must stay active.'),
('en', 'languageManager.loadError', 'Loading failed: {message}'),
('en', 'languageManager.saveError', 'Saving failed: {message}'),
('en', 'languageManager.deleteError', 'Deleting failed: {message}'),
('en', 'languageManager.importSuccess', '{count} terms imported.'),
('en', 'languageManager.importError', 'Import failed: {message}'),
('en', 'languageManager.importInvalidFile', 'The file could not be read (invalid JSON).'),
('en', 'languageManager.importMissingPlaceholder', '“{key}”: placeholder {placeholder} missing in the translation – row skipped.'),
('en', 'languageManager.duplicateCode', 'The language code “{code}” already exists.'),
('en', 'languageManager.invalidCode', 'Please enter a language code of 2-5 lowercase letters.');
insert into ui_translations (language_code, key, value) values
('pl', 'common.save', 'Zapisz'),
('pl', 'common.cancel', 'Anuluj'),
('pl', 'common.delete', 'Usuń'),
('pl', 'common.edit', 'Edytuj'),
('pl', 'common.add', 'Dodaj'),
('pl', 'common.back', 'Wstecz'),
('pl', 'common.login', 'Zaloguj się'),
('pl', 'common.logout', 'Wyloguj się'),
('pl', 'common.loading', 'Wczytywanie…'),
('pl', 'common.you', '(ty)'),
('pl', 'common.name', 'Nazwa'),
('pl', 'common.email', 'Adres e-mail'),
('pl', 'common.password', 'Hasło'),
('pl', 'common.role', 'Rola'),
('pl', 'common.roleAdmin', 'Administrator'),
('pl', 'common.roleStaff', 'Personel stołówki'),
('pl', 'weekdays.long.0', 'Poniedziałek'),
('pl', 'weekdays.long.1', 'Wtorek'),
('pl', 'weekdays.long.2', 'Środa'),
('pl', 'weekdays.long.3', 'Czwartek'),
('pl', 'weekdays.long.4', 'Piątek'),
('pl', 'weekdays.short.0', 'Pon'),
('pl', 'weekdays.short.1', 'Wto'),
('pl', 'weekdays.short.2', 'Śro'),
('pl', 'weekdays.short.3', 'Czw'),
('pl', 'weekdays.short.4', 'Pią'),
('pl', 'dateLocale', 'pl-PL'),
('pl', 'header.canteen', 'Stołówka'),
('pl', 'header.menuGroup', 'Jadłospis'),
('pl', 'header.weekPlan', 'Plan tygodnia'),
('pl', 'header.sections', 'Pory posiłków'),
('pl', 'header.dishes', 'Dania'),
('pl', 'header.labels', 'Alergeny i dodatki'),
('pl', 'header.adminGroup', 'Administracja'),
('pl', 'header.users', 'Użytkownicy'),
('pl', 'header.design', 'Wygląd'),
('pl', 'header.languages', 'Języki'),
('pl', 'header.suggestions', 'Propozycje'),
('pl', 'header.openSuggestions', '{count} otwartych propozycji dań'),
('pl', 'header.viewMenu', 'Zobacz jadłospis'),
('pl', 'header.myAccount', 'Moje konto'),
('pl', 'header.menuToggle', 'Otwórz/zamknij menu'),
('pl', 'login.title', 'Logowanie'),
('pl', 'login.submit', 'Zaloguj się'),
('pl', 'login.or', 'lub'),
('pl', 'login.passkeyButton', 'Zaloguj się kluczem dostępu'),
('pl', 'login.ssoButton', 'Zaloguj się przez Single Sign-On'),
('pl', 'login.backToMenu', '← Powrót do jadłospisu'),
('pl', 'login.errorInvalidCredentials', 'Nieprawidłowy adres e-mail lub hasło.'),
('pl', 'login.errorPasskeyCancelled', 'Logowanie kluczem dostępu zostało anulowane.'),
('pl', 'login.errorGeneric', 'Logowanie nie powiodło się: {message}'),
('pl', 'login.errorPasswordDisabled', 'Logowanie hasłem jest wyłączone dla tego konta. Zaloguj się kluczem dostępu.'),
('pl', 'login.errorOidcInvalidState', 'Logowanie SSO wygasło lub jest nieprawidłowe. Spróbuj ponownie.'),
('pl', 'login.errorOidcUnknownUser', 'Nie istnieje konto dla tego adresu e-mail. Skontaktuj się z administracją.'),
('pl', 'login.errorOidcFailed', 'Logowanie SSO nie powiodło się. Spróbuj ponownie.'),
('pl', 'publicMenu.prevWeek', '← Poprzedni tydzień'),
('pl', 'publicMenu.nextWeek', 'Następny tydzień →'),
('pl', 'publicMenu.prevWeekShort', '← Tydzień'),
('pl', 'publicMenu.nextWeekShort', 'Tydzień →'),
('pl', 'publicMenu.today', 'Dziś'),
('pl', 'publicMenu.dayTab', 'Dzień'),
('pl', 'publicMenu.weekTab', 'Tydzień'),
('pl', 'publicMenu.allergens', 'Alergeny'),
('pl', 'publicMenu.additives', 'Dodatki'),
('pl', 'publicMenu.labelingNoticeTitle', 'Alergeny i dodatki'),
('pl', 'publicMenu.suggestDish', 'Zaproponuj danie'),
('pl', 'publicMenu.noEntry', '–'),
('pl', 'publicMenu.rssFeed', 'Subskrybuj kanał RSS'),
('pl', 'weekEditor.title', 'Plan tygodnia'),
('pl', 'weekEditor.loading', 'Wczytywanie planu tygodnia…'),
('pl', 'weekEditor.changeDish', 'Wybierz inne danie'),
('pl', 'weekEditor.addMeal', 'Dodaj posiłek'),
('pl', 'weekEditor.newDishesHint', 'Nowe dania możesz dodać w zakładce {link}.'),
('pl', 'weekEditor.newDishesLinkLabel', 'Dania'),
('pl', 'weekEditor.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'weekEditor.duplicateEntry', 'To danie jest już przypisane na ten dzień.'),
('pl', 'weekEditor.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'weekEditor.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'dishManager.title', 'Dania'),
('pl', 'dishManager.newDish', 'Nowe danie'),
('pl', 'dishManager.editDish', 'Edytuj danie'),
('pl', 'dishManager.nameLabel', 'Nazwa'),
('pl', 'dishManager.nameEnLabel', 'Nazwa (angielski)'),
('pl', 'dishManager.namePlLabel', 'Nazwa (polski)'),
('pl', 'dishManager.autoTranslate', 'Przetłumacz automatycznie'),
('pl', 'dishManager.translating', 'Tłumaczenie…'),
('pl', 'dishManager.translated', 'Przetłumaczono.'),
('pl', 'dishManager.priceLabel', 'Cena w €'),
('pl', 'dishManager.pricePlaceholder', '3,50'),
('pl', 'dishManager.searchPlaceholder', 'Szukaj dania…'),
('pl', 'dishManager.selectAll', 'Zaznacz wszystko'),
('pl', 'dishManager.selectedCount', 'Zaznaczono: {count}'),
('pl', 'dishManager.deleteSelected', 'Usuń zaznaczone'),
('pl', 'dishManager.deleteSelectedCount', 'Usuń zaznaczone ({count})'),
('pl', 'dishManager.empty', 'Nie dodano jeszcze żadnych dań.'),
('pl', 'dishManager.noResults', 'Brak wyników dla „{query}”.'),
('pl', 'dishManager.confirmDelete', 'Czy na pewno usunąć „{name}”? Danie zostanie również usunięte ze wszystkich planów tygodniowych.'),
('pl', 'dishManager.confirmDeleteSelected', 'Czy na pewno usunąć {count} danie/dania? Zostaną również usunięte ze wszystkich planów tygodniowych.'),
('pl', 'dishManager.invalidPrice', 'Podaj prawidłową cenę, np. 3,50'),
('pl', 'dishManager.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'dishManager.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'dishManager.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'dishManager.exportButton', 'Eksportuj'),
('pl', 'dishManager.importButton', 'Importuj'),
('pl', 'dishManager.exportError', 'Eksport nie powiódł się: {message}'),
('pl', 'dishManager.importError', 'Import nie powiódł się: {message}'),
('pl', 'dishManager.importSuccess', 'Utworzono: {created}, zaktualizowano: {updated}.'),
('pl', 'dishManager.importSkipped', 'Pominięto wierszy: {count}: {reasons}'),
('pl', 'dishManager.importSkippedRow', 'Wiersz {row}: {reason}'),
('pl', 'dishManager.labelingDisabledHint', 'Alergeny/dodatki są obecnie wyłączone (aktywny tryb komunikatu, patrz „Alergeny i dodatki”).'),
('pl', 'codeList.allergensTitle', 'Alergeny'),
('pl', 'codeList.additivesTitle', 'Dodatki'),
('pl', 'codeList.allergenCodeLabel', 'Kod (np. A)'),
('pl', 'codeList.additiveCodeLabel', 'Kod (np. 1)'),
('pl', 'codeList.nameLabel', 'Nazwa'),
('pl', 'codeList.empty', 'Brak wpisów.'),
('pl', 'codeList.confirmDelete', 'Czy na pewno usunąć „{code} – {name}”?'),
('pl', 'codeList.duplicateCode', 'Kod „{code}” już istnieje.'),
('pl', 'codeList.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'codeList.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'codeList.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'codeList.exportButton', 'Eksportuj'),
('pl', 'codeList.importButton', 'Importuj'),
('pl', 'codeList.exportError', 'Eksport nie powiódł się: {message}'),
('pl', 'codeList.importError', 'Import nie powiódł się: {message}'),
('pl', 'codeList.importSuccess', 'Utworzono: {created}, zaktualizowano: {updated}.'),
('pl', 'codeList.importSkipped', 'Pominięto wierszy: {count}: {reasons}'),
('pl', 'codeList.importSkippedRow', 'Wiersz {row}: {reason}'),
('pl', 'sectionManager.title', 'Pory posiłków'),
('pl', 'sectionManager.newSection', 'Nowa pora posiłku'),
('pl', 'sectionManager.editSection', 'Edytuj porę posiłku'),
('pl', 'sectionManager.nameLabel', 'Nazwa'),
('pl', 'sectionManager.nameEnLabel', 'Nazwa (angielski)'),
('pl', 'sectionManager.namePlLabel', 'Nazwa (polski)'),
('pl', 'sectionManager.timeFrom', 'Od'),
('pl', 'sectionManager.timeTo', 'Do'),
('pl', 'sectionManager.moveUp', 'Przesuń w górę'),
('pl', 'sectionManager.moveDown', 'Przesuń w dół'),
('pl', 'sectionManager.empty', 'Nie dodano jeszcze żadnych pór posiłków.'),
('pl', 'sectionManager.confirmDelete', 'Czy na pewno usunąć „{name}”? Przypisane dania zostaną również usunięte ze wszystkich planów tygodniowych.'),
('pl', 'sectionManager.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'sectionManager.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'sectionManager.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'userManager.title', 'Użytkownicy'),
('pl', 'userManager.newUser', 'Nowy użytkownik'),
('pl', 'userManager.editUser', 'Edytuj użytkownika'),
('pl', 'userManager.newPasswordLabel', 'Nowe hasło (opcjonalnie)'),
('pl', 'userManager.repeatPasswordLabel', 'Powtórz hasło'),
('pl', 'userManager.passwordMismatch', 'Hasła nie są zgodne.'),
('pl', 'userManager.confirmDelete', 'Czy na pewno usunąć użytkownika „{email}”? Dostęp zostanie trwale usunięty.'),
('pl', 'userManager.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'designEditor.title', 'Wygląd'),
('pl', 'designEditor.logoTitle', 'Logo'),
('pl', 'designEditor.logoHint', 'Logo jest wyświetlane wyśrodkowane nad jadłospisem.'),
('pl', 'designEditor.logoNone', 'Nie ustawiono logo.'),
('pl', 'designEditor.logoUpload', 'Prześlij logo'),
('pl', 'designEditor.logoRemove', 'Usuń logo'),
('pl', 'designEditor.logoUpdated', 'Logo zaktualizowane.'),
('pl', 'designEditor.logoRemoved', 'Logo usunięte.'),
('pl', 'designEditor.faviconTitle', 'Favicon'),
('pl', 'designEditor.faviconHint', 'Wyświetlane jako ikona na karcie przeglądarki. Bez własnego favicona używana jest domyślna ikona aplikacji.'),
('pl', 'designEditor.faviconNone', 'Nie ustawiono własnego favicona.'),
('pl', 'designEditor.faviconUpload', 'Prześlij favicon'),
('pl', 'designEditor.faviconRemove', 'Usuń favicon'),
('pl', 'designEditor.faviconUpdated', 'Favicon zaktualizowany.'),
('pl', 'designEditor.faviconRemoved', 'Favicon usunięty.'),
('pl', 'designEditor.invalidImage', 'Wybierz plik graficzny (PNG, JPG, SVG, WebP, ICO).'),
('pl', 'designEditor.colorsTitle', 'Kolorystyka'),
('pl', 'designEditor.colorsSave', 'Zapisz kolory'),
('pl', 'designEditor.colorsReset', 'Przywróć domyślne'),
('pl', 'designEditor.colorsSaved', 'Kolory zapisane.'),
('pl', 'designEditor.colorPrimary', 'Kolor główny (nagłówek tabeli)'),
('pl', 'designEditor.colorPrimaryDark', 'Kolor główny, ciemny (górny pasek wewnętrzny)'),
('pl', 'designEditor.colorAccent', 'Kolor akcentu'),
('pl', 'designEditor.colorHeaderText', 'Kolor tekstu w nagłówku tabeli'),
('pl', 'designEditor.colorRowBg', 'Tło komórek'),
('pl', 'designEditor.colorRowBgAlt', 'Tło komórek (naprzemienne)'),
('pl', 'designEditor.colorAddBtn', 'Przycisk „Dodaj”'),
('pl', 'designEditor.colorDeleteBtn', 'Przycisk „Usuń”'),
('pl', 'designEditor.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'designEditor.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'designEditor.uploadError', 'Przesyłanie nie powiodło się: {message}'),
('pl', 'designEditor.confirmRemoveLogo', 'Czy na pewno usunąć logo?'),
('pl', 'designEditor.confirmRemoveFavicon', 'Czy na pewno usunąć favicon?'),
('pl', 'labelingNotice.title', 'Wyświetlanie alergenów i dodatków'),
('pl', 'labelingNotice.hint', 'Domyślnie każdy gość widzi szczegółowy wybór alergenów/dodatków dla każdego dania. Alternatywnie można zamiast tego wyświetlić tylko komunikat – wybór dla poszczególnych dań zostanie wtedy również ukryty w zarządzaniu daniami.'),
('pl', 'labelingNotice.modeDetailed', 'Pokaż szczegółowy wybór dla każdego dania'),
('pl', 'labelingNotice.modeNotice', 'Pokaż tylko komunikat'),
('pl', 'labelingNotice.noticeDeLabel', 'Tekst komunikatu (niemiecki)'),
('pl', 'labelingNotice.noticeEnLabel', 'Tekst komunikatu (angielski)'),
('pl', 'labelingNotice.noticePlLabel', 'Tekst komunikatu (polski)'),
('pl', 'labelingNotice.save', 'Zapisz'),
('pl', 'labelingNotice.saved', 'Zapisano.'),
('pl', 'labelingNotice.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'labelingNotice.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'passkeys.title', 'Klucze dostępu'),
('pl', 'passkeys.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 – po zmianie domeny trzeba je zarejestrować ponownie.'),
('pl', 'passkeys.register', 'Zarejestruj nowy klucz dostępu'),
('pl', 'passkeys.empty', 'Nie zarejestrowano jeszcze żadnych kluczy dostępu.'),
('pl', 'passkeys.createdOn', '– utworzono {date}'),
('pl', 'passkeys.registered', 'Klucz dostępu został pomyślnie zarejestrowany.'),
('pl', 'passkeys.registerError', 'Rejestracja nie powiodła się: {message}'),
('pl', 'passkeys.registerCancelled', 'Rejestracja klucza dostępu została anulowana.'),
('pl', 'passkeys.loadError', 'Nie udało się wczytać kluczy dostępu: {message}'),
('pl', 'passkeys.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'passkeys.confirmDelete', 'Czy na pewno usunąć ten klucz dostępu?'),
('pl', 'account.title', 'Moje konto'),
('pl', 'account.personalTitle', 'Dane osobowe'),
('pl', 'account.saveName', 'Zapisz nazwę'),
('pl', 'account.nameSaved', 'Nazwa zapisana.'),
('pl', 'account.nameRequired', 'Podaj nazwę.'),
('pl', 'account.emailTitle', 'Adres e-mail'),
('pl', 'account.changeEmail', 'Zmień adres e-mail'),
('pl', 'account.emailChanged', 'Adres e-mail zmieniony.'),
('pl', 'account.passwordTitle', 'Zmień hasło'),
('pl', 'account.currentPassword', 'Obecne hasło (do weryfikacji)'),
('pl', 'account.newPassword', 'Nowe hasło'),
('pl', 'account.newPasswordRepeat', 'Powtórz nowe hasło'),
('pl', 'account.changePassword', 'Zmień hasło'),
('pl', 'account.passwordChanged', 'Hasło zmienione.'),
('pl', 'account.passwordMismatch', 'Hasła nie są zgodne.'),
('pl', 'account.passwordTooShort', 'Nowe hasło musi mieć co najmniej 8 znaków.'),
('pl', 'account.passwordAuthTitle', 'Logowanie hasłem'),
('pl', 'account.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.'),
('pl', 'account.passwordAuthCheckbox', 'Logowanie e-mailem i hasłem dozwolone'),
('pl', 'account.passwordAuthEnabled', 'Logowanie hasłem włączone.'),
('pl', 'account.passwordAuthDisabled', 'Logowanie hasłem wyłączone.'),
('pl', 'account.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.'),
('pl', 'account.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.'),
('pl', 'account.wrongCurrentPassword', 'Obecne hasło jest nieprawidłowe.'),
('pl', 'account.emailAlreadyUsed', 'Ten adres e-mail jest już używany.'),
('pl', 'account.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'account.changeError', 'Zmiana nie powiodła się: {message}'),
('pl', 'suggestions.formTitle', 'Zaproponuj danie'),
('pl', 'suggestions.formHint', 'Brakuje dania w jadłospisie? Zaproponuj je tutaj – zespół stołówki sprawdzi Twoją propozycję.'),
('pl', 'suggestions.nameLabel', 'Nazwa dania'),
('pl', 'suggestions.descriptionLabel', 'Opis / składniki (opcjonalnie)'),
('pl', 'suggestions.submit', 'Wyślij propozycję'),
('pl', 'suggestions.submitting', 'Wysyłanie…'),
('pl', 'suggestions.success', 'Dziękujemy! Twoja propozycja została przesłana.'),
('pl', 'suggestions.duplicate', 'To danie już istnieje w jadłospisie.'),
('pl', 'suggestions.genericError', 'Wysyłanie nie powiodło się: {message}'),
('pl', 'suggestions.nameRequired', 'Podaj nazwę.'),
('pl', 'suggestions.backToMenu', '← Powrót do jadłospisu'),
('pl', 'suggestions.reviewTitle', 'Propozycje dań'),
('pl', 'suggestions.reviewEmpty', 'Brak propozycji.'),
('pl', 'suggestions.accept', 'Dodaj jako danie'),
('pl', 'suggestions.reject', 'Odrzuć'),
('pl', 'suggestions.confirmReject', 'Czy na pewno odrzucić i usunąć propozycję „{name}”?'),
('pl', 'suggestions.accepted', 'Dodano jako danie.'),
('pl', 'suggestions.rejected', 'Propozycja odrzucona.'),
('pl', 'suggestions.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'suggestions.actionError', 'Czynność nie powiodła się: {message}'),
('pl', 'suggestions.submittedOn', 'przesłano {date}'),
('pl', 'languageManager.title', 'Języki'),
('pl', 'languageManager.hint', 'Niemiecki jest stałym językiem bazowym i zawsze kompletny. Kolejne języki są importowane jako plik tłumaczenia i automatycznie uzupełniane niemieckim tekstem tam, gdzie brakuje tłumaczenia.'),
('pl', 'languageManager.codeColumn', 'Kod'),
('pl', 'languageManager.labelColumn', 'Nazwa wyświetlana'),
('pl', 'languageManager.activeColumn', 'Aktywny'),
('pl', 'languageManager.completenessColumn', 'Kompletność'),
('pl', 'languageManager.baseBadge', 'Język bazowy'),
('pl', 'languageManager.complete', 'Kompletny'),
('pl', 'languageManager.completeness', 'Przetłumaczono {percent}%'),
('pl', 'languageManager.outdated', 'Nowe terminy od ostatniego importu: {count}'),
('pl', 'languageManager.downloadTemplate', 'Pobierz szablon'),
('pl', 'languageManager.uploadTranslation', 'Prześlij tłumaczenie'),
('pl', 'languageManager.newLanguageTitle', 'Dodaj nowy język'),
('pl', 'languageManager.newLanguageCodeLabel', 'Kod języka (np. fr)'),
('pl', 'languageManager.newLanguageLabelLabel', 'Nazwa wyświetlana (np. Français)'),
('pl', 'languageManager.create', 'Utwórz'),
('pl', 'languageManager.delete', 'Usuń'),
('pl', 'languageManager.confirmDelete', 'Czy na pewno usunąć „{label}”? Wszystkie tłumaczenia tego języka zostaną utracone.'),
('pl', 'languageManager.cannotDeleteBase', 'Nie można usunąć języka bazowego.'),
('pl', 'languageManager.cannotDeactivateLast', 'Co najmniej jeden język musi pozostać aktywny.'),
('pl', 'languageManager.loadError', 'Wczytywanie nie powiodło się: {message}'),
('pl', 'languageManager.saveError', 'Zapisywanie nie powiodło się: {message}'),
('pl', 'languageManager.deleteError', 'Usuwanie nie powiodło się: {message}'),
('pl', 'languageManager.importSuccess', 'Zaimportowano terminów: {count}.'),
('pl', 'languageManager.importError', 'Import nie powiódł się: {message}'),
('pl', 'languageManager.importInvalidFile', 'Nie można odczytać pliku (nieprawidłowy JSON).'),
('pl', 'languageManager.importMissingPlaceholder', '„{key}”: brak symbolu zastępczego {placeholder} w tłumaczeniu – wiersz pominięto.'),
('pl', 'languageManager.duplicateCode', 'Kod języka „{code}” już istnieje.'),
('pl', 'languageManager.invalidCode', 'Podaj kod języka złożony z 2–5 małych liter.');