diff --git a/public/i18n.js b/public/i18n.js index ee3f587..b45a0fe 100644 --- a/public/i18n.js +++ b/public/i18n.js @@ -9,7 +9,10 @@ const SUPPORTED_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'sv', 'el', 'ru', 'tr', const DEFAULT_LOCALE = 'de'; const STORAGE_KEY = 'oikos-locale'; const DATE_FORMAT_KEY = 'oikos-date-format'; +const TIME_FORMAT_KEY = 'oikos-time-format'; const DEFAULT_DATE_FORMAT = 'dmy'; +const DEFAULT_TIME_FORMAT = '24h'; +const VALID_TIME_FORMATS = ['24h', '12h']; let currentLocale = DEFAULT_LOCALE; let translations = {}; @@ -93,6 +96,15 @@ export function getDateFormat() { return getDateFormatPreference(); } +function getTimeFormatPreference() { + const stored = localStorage.getItem(TIME_FORMAT_KEY); + return VALID_TIME_FORMATS.includes(stored) ? stored : DEFAULT_TIME_FORMAT; +} + +export function getTimeFormat() { + return getTimeFormatPreference(); +} + function formatDateParts(date, useUtc = false) { const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return ''; @@ -176,9 +188,78 @@ export function formatTime(date) { if (date == null) return ''; const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return ''; + if (getTimeFormatPreference() === '12h') { + const hour = d.getHours(); + const minute = String(d.getMinutes()).padStart(2, '0'); + const displayHour = hour % 12 || 12; + return `${displayHour}:${minute} ${hour >= 12 ? 'PM' : 'AM'}`; + } return new Intl.DateTimeFormat(currentLocale, { hour: '2-digit', minute: '2-digit', hour12: false, }).format(d); } + +function toTimeParts(value) { + if (value == null || value === '') return null; + + if (value instanceof Date) { + if (isNaN(value.getTime())) return null; + return { hour: value.getHours(), minute: value.getMinutes() }; + } + + const raw = String(value).trim(); + if (!raw) return null; + + if (/^\d{1,2}:\d{2}$/.test(raw)) { + const [hour, minute] = raw.split(':').map(Number); + if (hour >= 0 && hour < 24 && minute >= 0 && minute < 60) { + return { hour, minute }; + } + return null; + } + + const ampmMatch = raw.match(/^(\d{1,2})(?::(\d{2}))?\s*([ap]m)$/i); + if (ampmMatch) { + let hour = Number(ampmMatch[1]); + const minute = Number(ampmMatch[2] ?? 0); + const meridiem = ampmMatch[3].toLowerCase(); + if (!Number.isInteger(hour) || !Number.isInteger(minute) || minute < 0 || minute >= 60) return null; + if (hour < 1 || hour > 12) return null; + if (meridiem === 'pm' && hour !== 12) hour += 12; + if (meridiem === 'am' && hour === 12) hour = 0; + return { hour, minute }; + } + + return null; +} + +export function formatTimeInput(value) { + const parts = toTimeParts(value); + if (!parts) return ''; + const hour = String(parts.hour).padStart(2, '0'); + const minute = String(parts.minute).padStart(2, '0'); + if (getTimeFormatPreference() === '12h') { + const isPm = parts.hour >= 12; + const displayHour = parts.hour % 12 || 12; + return `${displayHour}:${minute} ${isPm ? 'PM' : 'AM'}`; + } + return `${hour}:${minute}`; +} + +export function parseTimeInput(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + const parts = toTimeParts(raw); + if (!parts) return ''; + return `${String(parts.hour).padStart(2, '0')}:${String(parts.minute).padStart(2, '0')}`; +} + +export function isTimeInputValid(value) { + return !String(value || '').trim() || !!parseTimeInput(value); +} + +export function timeInputPlaceholder() { + return getTimeFormatPreference() === '12h' ? 'h:mm AM/PM' : 'HH:MM'; +} diff --git a/public/locales/ar.json b/public/locales/ar.json index 9dc25ef..e247dd4 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -607,6 +607,9 @@ "dateFormatLabel": "تنسيق التاريخ المفضل", "dateFormatHint": "اختر كيف تظهر التواريخ في التطبيق.", "dateFormatSavedToast": "تم حفظ تنسيق التاريخ.", + "timeFormatLabel": "تنسيق الوقت", + "timeFormatHours": "ساعة", + "timeFormatSavedToast": "تم حفظ تنسيق الوقت.", "themeSystem": "النظام", "themeSysLabel": "استخدام إعداد النظام", "themeLight": "فاتح", diff --git a/public/locales/de.json b/public/locales/de.json index 09be1e2..aec8962 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -632,6 +632,9 @@ "dateFormatLabel": "Bevorzugtes Datumsformat", "dateFormatHint": "Wähle, wie Daten in der App angezeigt werden.", "dateFormatSavedToast": "Datumsformat gespeichert.", + "timeFormatLabel": "Zeitformat", + "timeFormatHours": "Stunden", + "timeFormatSavedToast": "Zeitformat gespeichert.", "themeSystem": "System", "themeSysLabel": "System-Einstellung verwenden", "themeLight": "Hell", diff --git a/public/locales/el.json b/public/locales/el.json index 3cd0e17..e3230bb 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Προτιμώμενη μορφή ημερομηνίας", "dateFormatHint": "Επιλέξτε πώς εμφανίζονται οι ημερομηνίες στην εφαρμογή.", "dateFormatSavedToast": "Η μορφή ημερομηνίας αποθηκεύτηκε.", + "timeFormatLabel": "Μορφή ώρας", + "timeFormatHours": "ώρες", + "timeFormatSavedToast": "Η μορφή ώρας αποθηκεύτηκε.", "themeSystem": "Σύστημα", "themeSysLabel": "Χρήση ρύθμισης συστήματος", "themeLight": "Ανοιχτό", diff --git a/public/locales/en.json b/public/locales/en.json index b9d3157..21462d9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Preferred date format", "dateFormatHint": "Choose how dates are displayed throughout the app.", "dateFormatSavedToast": "Date format saved.", + "timeFormatLabel": "Time format", + "timeFormatHours": "hours", + "timeFormatSavedToast": "Time format saved.", "themeSystem": "System", "themeSysLabel": "Use system setting", "themeLight": "Light", @@ -1019,4 +1022,4 @@ "dropzoneHint": "Drag a file into this area, or use the file picker.", "selectedFileLabel": "Selected: {{name}}" } -} \ No newline at end of file +} diff --git a/public/locales/es.json b/public/locales/es.json index 5410d31..6c4ca09 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Formato de fecha preferido", "dateFormatHint": "Elige cómo se muestran las fechas en toda la app.", "dateFormatSavedToast": "Formato de fecha guardado.", + "timeFormatLabel": "Formato de hora", + "timeFormatHours": "horas", + "timeFormatSavedToast": "Formato de hora guardado.", "themeSystem": "Sistema", "themeSysLabel": "Usar configuración del sistema", "themeLight": "Claro", diff --git a/public/locales/fr.json b/public/locales/fr.json index 9641f44..d804651 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Format de date préféré", "dateFormatHint": "Choisissez comment les dates sont affichées dans l'application.", "dateFormatSavedToast": "Format de date enregistré.", + "timeFormatLabel": "Format de l'heure", + "timeFormatHours": "heures", + "timeFormatSavedToast": "Format de l'heure enregistré.", "themeSystem": "Système", "themeSysLabel": "Utiliser le paramètre système", "themeLight": "Clair", diff --git a/public/locales/hi.json b/public/locales/hi.json index e64aa80..028f1a1 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -607,6 +607,9 @@ "dateFormatLabel": "पसंदीदा तारीख प्रारूप", "dateFormatHint": "चुनें कि ऐप में तारीखें कैसे दिखाई दें।", "dateFormatSavedToast": "तारीख प्रारूप सहेजा गया।", + "timeFormatLabel": "समय प्रारूप", + "timeFormatHours": "घंटे", + "timeFormatSavedToast": "समय प्रारूप सहेजा गया।", "themeSystem": "सिस्टम", "themeSysLabel": "सिस्टम सेटिंग का उपयोग करें", "themeLight": "हल्का", diff --git a/public/locales/it.json b/public/locales/it.json index 4bb0782..f8dceed 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Formato data preferito", "dateFormatHint": "Scegli come vengono mostrate le date nell'app.", "dateFormatSavedToast": "Formato data salvato.", + "timeFormatLabel": "Formato orario", + "timeFormatHours": "ore", + "timeFormatSavedToast": "Formato orario salvato.", "themeSystem": "Sistema", "themeSysLabel": "Usa impostazione di sistema", "themeLight": "Chiaro", diff --git a/public/locales/ja.json b/public/locales/ja.json index 2dc64a4..fa047db 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -607,6 +607,9 @@ "dateFormatLabel": "希望する日付形式", "dateFormatHint": "アプリ内で日付をどう表示するかを選択します。", "dateFormatSavedToast": "日付形式を保存しました。", + "timeFormatLabel": "時刻形式", + "timeFormatHours": "時間", + "timeFormatSavedToast": "時刻形式を保存しました。", "themeSystem": "システム設定", "themeSysLabel": "システム設定を使用", "themeLight": "ライト", diff --git a/public/locales/pt.json b/public/locales/pt.json index 612b633..32b1e7f 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Formato preferido da data", "dateFormatHint": "Escolha como as datas aparecem em toda a aplicação.", "dateFormatSavedToast": "Formato da data salvo.", + "timeFormatLabel": "Formato de hora", + "timeFormatHours": "horas", + "timeFormatSavedToast": "Formato de hora salvo.", "themeSystem": "Sistema", "themeSysLabel": "Usar configuração do sistema", "themeLight": "Claro", diff --git a/public/locales/ru.json b/public/locales/ru.json index 60e5983..8de0150 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Предпочитаемый формат даты", "dateFormatHint": "Выберите, как даты отображаются в приложении.", "dateFormatSavedToast": "Формат даты сохранён.", + "timeFormatLabel": "Формат времени", + "timeFormatHours": "часов", + "timeFormatSavedToast": "Формат времени сохранён.", "themeSystem": "Система", "themeSysLabel": "Использовать системную настройку", "themeLight": "Светлая", diff --git a/public/locales/sv.json b/public/locales/sv.json index 27b17e5..37a2198 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Önskat datumformat", "dateFormatHint": "Välj hur datum visas i appen.", "dateFormatSavedToast": "Datumformat sparat.", + "timeFormatLabel": "Tidsformat", + "timeFormatHours": "timmar", + "timeFormatSavedToast": "Tidsformat sparat.", "themeSystem": "System", "themeSysLabel": "Använd systeminställning", "themeLight": "Ljus", diff --git a/public/locales/tr.json b/public/locales/tr.json index 8cda108..e0e9dbd 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Tercih edilen tarih biçimi", "dateFormatHint": "Tarihlerin uygulamada nasıl görüneceğini seçin.", "dateFormatSavedToast": "Tarih biçimi kaydedildi.", + "timeFormatLabel": "Saat biçimi", + "timeFormatHours": "saat", + "timeFormatSavedToast": "Saat biçimi kaydedildi.", "themeSystem": "Sistem", "themeSysLabel": "Sistem ayarını kullan", "themeLight": "Açık", diff --git a/public/locales/uk.json b/public/locales/uk.json index 272cde9..7d97282 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -607,6 +607,9 @@ "dateFormatLabel": "Бажаний формат дати", "dateFormatHint": "Виберіть, як дати відображаються в застосунку.", "dateFormatSavedToast": "Формат дати збережено.", + "timeFormatLabel": "Формат часу", + "timeFormatHours": "годин", + "timeFormatSavedToast": "Формат часу збережено.", "themeSystem": "Системна", "themeSysLabel": "Використовувати системні налаштування", "themeLight": "Світла", diff --git a/public/locales/zh.json b/public/locales/zh.json index e0ef6e8..babffb6 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -607,6 +607,9 @@ "dateFormatLabel": "首选日期格式", "dateFormatHint": "选择日期在应用中的显示方式。", "dateFormatSavedToast": "日期格式已保存。", + "timeFormatLabel": "时间格式", + "timeFormatHours": "小时", + "timeFormatSavedToast": "时间格式已保存。", "themeSystem": "跟随系统", "themeSysLabel": "使用系统设置", "themeLight": "浅色", diff --git a/public/pages/calendar.js b/public/pages/calendar.js index 1553d7e..d5cb96a 100644 --- a/public/pages/calendar.js +++ b/public/pages/calendar.js @@ -8,7 +8,7 @@ import { api } from '/api.js'; import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; import { stagger } from '/utils/ux.js'; -import { t, formatDate as formatPreferredDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js'; +import { t, formatDate as formatPreferredDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, formatTimeInput, parseTimeInput, timeInputPlaceholder } from '/i18n.js'; import { esc, fmtLocation } from '/utils/html.js'; import { refresh as refreshReminders } from '/reminders.js'; @@ -171,6 +171,7 @@ const EVENT_ICONS = [ const CUSTOM_EVENT_ICONS = new Set(['tooth']); const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024; const ATTACHMENT_IMAGE_MIME = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']); +const CALENDAR_VIEW_STORAGE_KEY = 'oikos-calendar-view'; const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht @@ -212,6 +213,22 @@ let _container = null; function pad(n) { return String(n).padStart(2, '0'); } function isoDate(d) { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } +function getSavedCalendarView() { + try { + const saved = localStorage.getItem(CALENDAR_VIEW_STORAGE_KEY); + return VIEWS.includes(saved) ? saved : 'month'; + } catch { + return 'month'; + } +} + +function setSavedCalendarView(view) { + if (!VIEWS.includes(view)) return; + try { + localStorage.setItem(CALENDAR_VIEW_STORAGE_KEY, view); + } catch {} +} + // Extract YYYY-MM-DD in the browser's local timezone from any datetime string. // For date-only strings (≤10 chars) slicing is safe; for datetime strings with an // explicit UTC offset or 'Z' suffix, new Date() converts to local before extraction. @@ -438,7 +455,7 @@ export async function render(container, { user }) { _container = container; state.today = isoDate(new Date()); state.cursor = state.today; - state.view = 'month'; + state.view = getSavedCalendarView(); container.innerHTML = `