diff --git a/public/i18n.js b/public/i18n.js index 64b88d9..9228070 100644 --- a/public/i18n.js +++ b/public/i18n.js @@ -89,6 +89,10 @@ function getDateFormatPreference() { return ['mdy', 'dmy', 'ymd'].includes(stored) ? stored : DEFAULT_DATE_FORMAT; } +export function getDateFormat() { + return getDateFormatPreference(); +} + function formatDateParts(date, useUtc = false) { const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return ''; @@ -121,6 +125,52 @@ export function formatDate(date) { return formatDateParts(date); } +export function dateInputPlaceholder() { + switch (getDateFormatPreference()) { + case 'dmy': return 'DD/MM/YYYY'; + case 'ymd': return 'YYYY-MM-DD'; + default: return 'MM/DD/YYYY'; + } +} + +export function formatDateInput(date) { + if (!date) return ''; + return formatDate(date); +} + +export function parseDateInput(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + + const isoMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (isoMatch) return isValidDateParts(isoMatch[1], isoMatch[2], isoMatch[3]) ? raw : ''; + + const slashMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); + if (!slashMatch) return ''; + + const [, first, second, year] = slashMatch; + const [month, day] = getDateFormatPreference() === 'dmy' + ? [second, first] + : [first, second]; + + if (!isValidDateParts(year, month, day)) return ''; + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; +} + +export function isDateInputValid(value) { + const raw = String(value || '').trim(); + return !raw || !!parseDateInput(raw); +} + +function isValidDateParts(year, month, day) { + const y = Number(year); + const m = Number(month); + const d = Number(day); + if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return false; + const date = new Date(Date.UTC(y, m - 1, d)); + return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d; +} + /** Uhrzeit locale-aware formatieren */ export function formatTime(date) { if (date == null) return ''; diff --git a/public/locales/ar.json b/public/locales/ar.json index 35e3ed1..86517da 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -349,7 +349,9 @@ "ics": { "reset": "إعادة التعيين للأصل", "resetToast": "تم إعادة تعيين التغييرات." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "لوحة الملاحظات", @@ -856,7 +858,17 @@ "notificationEnable": "تفعيل الإشعارات", "notificationEnabled": "الإشعارات نشطة", "notificationDenied": "الإشعارات محظورة", - "notificationHint": "احصل على إشعارات حتى عندما يكون التطبيق مفتوحًا." + "notificationHint": "احصل على إشعارات حتى عندما يكون التطبيق مفتوحًا.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/de.json b/public/locales/de.json index 66c73cc..798a228 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -366,7 +366,9 @@ "ics": { "reset": "Auf Original zurücksetzen", "resetToast": "Änderungen zurückgesetzt." - } + }, + "iconLabel": "Icon", + "invalidDate": "Bitte ein gültiges Datum im ausgewählten Format verwenden." }, "notes": { "title": "Notizen", @@ -823,7 +825,17 @@ "notificationDenied": "Benachrichtigungen blockiert", "notificationHint": "Erhalte Benachrichtigungen auch wenn die App geöffnet ist.", "pendingBadgeTitle": "{{count}} fällige Erinnerung", - "pendingBadgeTitlePlural": "{{count}} fällige Erinnerungen" + "pendingBadgeTitlePlural": "{{count}} fällige Erinnerungen", + "offset2days": "2 Tage vorher", + "offset1week": "1 Woche vorher", + "offset2weeks": "2 Wochen vorher", + "offsetCustom": "Benutzerdefiniert...", + "customAmountLabel": "Anzahl", + "customUnitLabel": "Einheit", + "customMinutes": "Minuten", + "customHours": "Stunden", + "customDays": "Tage", + "customWeeks": "Wochen" }, "birthdays": { "title": "Geburtstage", @@ -893,24 +905,24 @@ "banner": "Offline – Verbindung wird wiederhergestellt…" }, "emptyHint": { - "tasks": "Tippe auf + um deine erste Aufgabe zu erstellen. Wische eine Karte nach links zum Löschen.", + "tasks": "Tippe auf + um deine erste Aufgabe zu erstellen. Wische eine Karte nach links zum Löschen.", "calendar": "Verbinde Google Kalender unter Einstellungen → Integrationen für automatische Synchronisation.", "shopping": "Füge Artikel hinzu und wische zum Abhaken oder Löschen.", - "notes": "Tippe auf + für eine neue Notiz. Notizen werden im Volltext durchsucht.", + "notes": "Tippe auf + für eine neue Notiz. Notizen werden im Volltext durchsucht.", "contacts": "Lege wichtige Kontakte an — Arzt, Schule, Notfall — für Schnellzugriff.", - "budget": "Erstelle Kategorien und trage Einnahmen und Ausgaben ein.", - "meals": "Plane Mahlzeiten für die Woche und verknüpfe Rezepte.", + "budget": "Erstelle Kategorien und trage Einnahmen und Ausgaben ein.", + "meals": "Plane Mahlzeiten für die Woche und verknüpfe Rezepte.", "birthdays": "Trage Geburtstage ein — du erhältst eine Erinnerung rechtzeitig.", - "recipes": "Lege Rezepte an und verknüpfe sie mit deiner Mahlzeitenplanung." + "recipes": "Lege Rezepte an und verknüpfe sie mit deiner Mahlzeitenplanung." }, "shortcuts": { - "search": "Suche öffnen", - "new": "Neuen Eintrag erstellen", - "help": "Tastenkombinationen", - "goDash": "Dashboard", + "search": "Suche öffnen", + "new": "Neuen Eintrag erstellen", + "help": "Tastenkombinationen", + "goDash": "Dashboard", "goTasks": "Aufgaben", - "goCal": "Kalender", - "goShop": "Einkaufsliste", + "goCal": "Kalender", + "goShop": "Einkaufsliste", "goNotes": "Notizen" } } diff --git a/public/locales/el.json b/public/locales/el.json index f61fb32..2adff64 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -349,7 +349,9 @@ "ics": { "reset": "Επαναφορά στο αρχικό", "resetToast": "Οι αλλαγές επαναφέρθηκαν." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Σημειώσεις", @@ -856,7 +858,17 @@ "notificationEnable": "Ενεργοποίηση ειδοποιήσεων", "notificationEnabled": "Ειδοποιήσεις ενεργές", "notificationDenied": "Ειδοποιήσεις αποκλεισμένες", - "notificationHint": "Λάβετε ειδοποιήσεις ακόμα και όταν η εφαρμογή είναι ανοιχτή." + "notificationHint": "Λάβετε ειδοποιήσεις ακόμα και όταν η εφαρμογή είναι ανοιχτή.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/en.json b/public/locales/en.json index 76c18b2..fc3cb6a 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -349,7 +349,9 @@ "ics": { "reset": "Reset to original", "resetToast": "Changes reset." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Board", @@ -798,7 +800,17 @@ "notificationDenied": "Notifications blocked", "notificationHint": "Receive notifications while the app is open.", "pendingBadgeTitle": "{{count}} reminder due", - "pendingBadgeTitlePlural": "{{count}} reminders due" + "pendingBadgeTitlePlural": "{{count}} reminders due", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "birthdays": { "title": "Birthdays", @@ -894,4 +906,4 @@ "birthdays": "Add birthdays — you will receive a reminder in time.", "recipes": "Create recipes and link them to your meal planner." } -} \ No newline at end of file +} diff --git a/public/locales/es.json b/public/locales/es.json index c5adbf3..0a0ab1d 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -349,7 +349,9 @@ "ics": { "reset": "Restaurar original", "resetToast": "Cambios restablecidos." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Notas", @@ -856,7 +858,17 @@ "notificationEnable": "Activar notificaciones", "notificationEnabled": "Notificaciones activas", "notificationDenied": "Notificaciones bloqueadas", - "notificationHint": "Recibe notificaciones incluso cuando la aplicación está abierta." + "notificationHint": "Recibe notificaciones incluso cuando la aplicación está abierta.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/fr.json b/public/locales/fr.json index 52f6f5d..95164a3 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -349,7 +349,9 @@ "ics": { "reset": "Réinitialiser", "resetToast": "Modifications annulées." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Notes", @@ -856,7 +858,17 @@ "notificationEnable": "Activer les notifications", "notificationEnabled": "Notifications actives", "notificationDenied": "Notifications bloquées", - "notificationHint": "Recevez des notifications même lorsque l'application est ouverte." + "notificationHint": "Recevez des notifications même lorsque l'application est ouverte.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/hi.json b/public/locales/hi.json index 3652c2d..5f38fb0 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -349,7 +349,9 @@ "ics": { "reset": "मूल पर वापस जाएं", "resetToast": "परिवर्तन रीसेट हो गए।" - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "नोट बोर्ड", @@ -856,7 +858,17 @@ "notificationEnable": "सूचनाएं सक्षम करें", "notificationEnabled": "सूचनाएं सक्रिय", "notificationDenied": "सूचनाएं अवरुद्ध", - "notificationHint": "ऐप खुली होने पर भी सूचनाएं प्राप्त करें।" + "notificationHint": "ऐप खुली होने पर भी सूचनाएं प्राप्त करें।", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/it.json b/public/locales/it.json index e823a10..f316596 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -349,7 +349,9 @@ "ics": { "reset": "Ripristina originale", "resetToast": "Modifiche ripristinate." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Bacheca", @@ -856,7 +858,17 @@ "notificationEnable": "Attiva notifiche", "notificationEnabled": "Notifiche attive", "notificationDenied": "Notifiche bloccate", - "notificationHint": "Ricevi notifiche anche quando l'app è aperta." + "notificationHint": "Ricevi notifiche anche quando l'app è aperta.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/ja.json b/public/locales/ja.json index 99532e5..dbda8e5 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -349,7 +349,9 @@ "ics": { "reset": "元に戻す", "resetToast": "変更がリセットされました。" - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "メモボード", @@ -856,7 +858,17 @@ "notificationEnable": "通知を有効にする", "notificationEnabled": "通知が有効", "notificationDenied": "通知がブロックされています", - "notificationHint": "アプリが開いているときでも通知を受け取ります。" + "notificationHint": "アプリが開いているときでも通知を受け取ります。", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/pt.json b/public/locales/pt.json index 63f71a7..82e226f 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -349,7 +349,9 @@ "ics": { "reset": "Restaurar original", "resetToast": "Alterações restauradas." - } + }, + "iconLabel": "Ícone", + "invalidDate": "Use uma data válida no formato selecionado." }, "notes": { "title": "Quadro de notas", @@ -857,7 +859,17 @@ "notificationEnable": "Ativar notificações", "notificationEnabled": "Notificações ativas", "notificationDenied": "Notificações bloqueadas", - "notificationHint": "Receba notificações mesmo quando a aplicação está aberta." + "notificationHint": "Receba notificações mesmo quando a aplicação está aberta.", + "offset2days": "2 dias antes", + "offset1week": "1 semana antes", + "offset2weeks": "2 semanas antes", + "offsetCustom": "Personalizado...", + "customAmountLabel": "Quantidade", + "customUnitLabel": "Unidade", + "customMinutes": "Minutos", + "customHours": "Horas", + "customDays": "Dias", + "customWeeks": "Semanas" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -873,4 +885,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/ru.json b/public/locales/ru.json index 354d8d4..4590170 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -349,7 +349,9 @@ "ics": { "reset": "Сбросить к исходному", "resetToast": "Изменения сброшены." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Заметки", @@ -856,7 +858,17 @@ "notificationEnable": "Включить уведомления", "notificationEnabled": "Уведомления активны", "notificationDenied": "Уведомления заблокированы", - "notificationHint": "Получайте уведомления, даже когда приложение открыто." + "notificationHint": "Получайте уведомления, даже когда приложение открыто.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/sv.json b/public/locales/sv.json index b3871f1..b16347b 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -349,7 +349,9 @@ "ics": { "reset": "Återställ till original", "resetToast": "Ändringar återställda." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Anteckningar", @@ -856,7 +858,17 @@ "notificationEnable": "Aktivera notiser", "notificationEnabled": "Notiser aktiva", "notificationDenied": "Notiser blockerade", - "notificationHint": "Få notiser även när appen är öppen." + "notificationHint": "Få notiser även när appen är öppen.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/tr.json b/public/locales/tr.json index b6d8720..1d3352e 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -349,7 +349,9 @@ "ics": { "reset": "Orijinale sıfırla", "resetToast": "Değişiklikler sıfırlandı." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Notlar", @@ -856,7 +858,17 @@ "notificationEnable": "Bildirimleri etkinleştir", "notificationEnabled": "Bildirimler etkin", "notificationDenied": "Bildirimler engellendi", - "notificationHint": "Uygulama açıkken bile bildirim alın." + "notificationHint": "Uygulama açıkken bile bildirim alın.", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/uk.json b/public/locales/uk.json index b7caa69..a124847 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -349,7 +349,9 @@ "ics": { "reset": "Скинути до оригіналу", "resetToast": "Зміни скинуто." - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "Нотатки", @@ -798,7 +800,17 @@ "notificationDenied": "Сповіщення заблоковано", "notificationHint": "Отримуйте сповіщення, поки додаток відкрито.", "pendingBadgeTitle": "{{count}} нагадування", - "pendingBadgeTitlePlural": "{{count}} нагадувань" + "pendingBadgeTitlePlural": "{{count}} нагадувань", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "recipes": { "title": "Recipes", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/locales/zh.json b/public/locales/zh.json index abfb61a..597fb3e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -349,7 +349,9 @@ "ics": { "reset": "重置为原始", "resetToast": "更改已重置。" - } + }, + "iconLabel": "Icon", + "invalidDate": "Use a valid date in the selected date format." }, "notes": { "title": "便签板", @@ -856,7 +858,17 @@ "notificationEnable": "启用通知", "notificationEnabled": "通知已启用", "notificationDenied": "通知已被阻止", - "notificationHint": "即使应用程序打开时也能收到通知。" + "notificationHint": "即使应用程序打开时也能收到通知。", + "offset2days": "2 days before", + "offset1week": "1 week before", + "offset2weeks": "2 weeks before", + "offsetCustom": "Custom...", + "customAmountLabel": "Amount", + "customUnitLabel": "Unit", + "customMinutes": "Minutes", + "customHours": "Hours", + "customDays": "Days", + "customWeeks": "Weeks" }, "onboarding": { "step1Title": "Welcome to Oikos", @@ -872,4 +884,4 @@ "offline": { "banner": "Offline – reconnecting…" } -} \ No newline at end of file +} diff --git a/public/pages/birthdays.js b/public/pages/birthdays.js index 47a92ba..0f12ac5 100644 --- a/public/pages/birthdays.js +++ b/public/pages/birthdays.js @@ -1,7 +1,7 @@ import { api } from '/api.js'; import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js'; import { stagger, deleteWithUndo } from '/utils/ux.js'; -import { t, formatDate } from '/i18n.js'; +import { t, formatDate, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js'; import { esc } from '/utils/html.js'; let state = { @@ -282,7 +282,7 @@ function openBirthdayModal({ mode, birthday = null }) {