Adds Swiss German, French, Italian, Dutch, Danish, Norwegian, Swedish and Finnish — ten selectable languages, each complete at 418/418 keys with matching placeholders and date arrays. A language used to be registered in four unrelated places (dictionary, settings dropdown, date-picker locale ternary, resolver fallback), so adding one meant touching all of them. LANGUAGES in i18n.js is now the single source of truth: adding a language is one entry plus one dictionary, and the dropdown derives from it. The registry also separates the stored code from the BCP-47 tag used for formatting. That distinction is what makes 'ch' safe — "ch" is a country, not a language, and would throw if handed to Intl; it maps to de-CH. Fixes locale handling along the way: fmtTime/fmtDatetime in calendar.js, month.js and week.js hardcoded 'de', so English users saw German-formatted times, and two toLocaleDateString() calls silently used the browser locale instead of the app language. All formatting now goes through getLocale(). Missing keys fall back language → English → German rather than straight to German, and the dead per-dictionary `locale` key is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.9 KiB
JavaScript
106 lines
3.9 KiB
JavaScript
import { isPast, eventTitle, birthdayIconSvg } from '../utils.js';
|
||
import { t, getLocale } from '../i18n.js';
|
||
|
||
export function renderAgenda(container, currentDate, events, onEventClick) {
|
||
if (!events.length) {
|
||
container.innerHTML = `<div class="agenda-view"><div class="agenda-empty">${t('no_events')}</div></div>`;
|
||
return;
|
||
}
|
||
|
||
// Group events by date
|
||
const groups = {};
|
||
events.forEach(ev => {
|
||
const d = new Date(ev.start);
|
||
const key = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
||
if (!groups[key]) groups[key] = [];
|
||
groups[key].push(ev);
|
||
});
|
||
|
||
// Sort groups
|
||
const sortedKeys = Object.keys(groups).sort();
|
||
|
||
const html = sortedKeys.map(key => {
|
||
const date = new Date(key + 'T00:00:00');
|
||
const isToday = isTodayDate(date);
|
||
const todayCls = isToday ? 'today' : '';
|
||
|
||
const evHtml = groups[key]
|
||
.sort((a, b) => {
|
||
if (a.allDay && !b.allDay) return -1;
|
||
if (!a.allDay && b.allDay) return 1;
|
||
return new Date(a.start) - new Date(b.start);
|
||
})
|
||
.map(ev => {
|
||
const color = ev.color || ev.calendarColor || '#4285f4';
|
||
const pastCls = isPast(ev) ? 'past' : '';
|
||
let timeStr = t('allday_cap');
|
||
if (!ev.allDay) {
|
||
const s = new Date(ev.start);
|
||
const e = new Date(ev.end);
|
||
timeStr = `${fmtTime(s)} – ${fmtTime(e)}`;
|
||
}
|
||
const locHtml = ev.location
|
||
? `<span class="agenda-ev-meta"> · ${escHtml(ev.location)}</span>`
|
||
: '';
|
||
return `<div class="agenda-event ${pastCls}" data-id="${ev.id}" data-url="${escAttr(ev.url)}">
|
||
<div class="agenda-ev-color" style="background:${color}"></div>
|
||
<div class="agenda-ev-info">
|
||
<div class="agenda-ev-title">${ev.is_birthday ? birthdayIconSvg() : ''}${escHtml(eventTitle(ev))}</div>
|
||
<div class="agenda-ev-meta">${timeStr}${locHtml}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
return `<div class="agenda-day" data-date="${key}">
|
||
<div class="agenda-date ${todayCls}">
|
||
<div class="agenda-date-num">${date.getDate()}</div>
|
||
<div class="agenda-date-label">
|
||
<span class="wd">${t('days_long')[date.getDay()]}</span>
|
||
<span class="mo">${t('months_short')[date.getMonth()]} ${date.getFullYear()}</span>
|
||
</div>
|
||
</div>
|
||
${evHtml}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `<div class="agenda-view">${html}</div>`;
|
||
|
||
// The agenda lists the whole cached range (past + future). Scroll so the
|
||
// current date (e.g. "today" after the Today button) sits at the top — or
|
||
// the next day with events if the current date itself has none.
|
||
const scrollEl = container.querySelector('.agenda-view');
|
||
if (scrollEl) {
|
||
const curKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth()+1).padStart(2,'0')}-${String(currentDate.getDate()).padStart(2,'0')}`;
|
||
const days = [...scrollEl.querySelectorAll('.agenda-day')];
|
||
const target = days.find(d => d.dataset.date >= curKey) || days[days.length - 1];
|
||
if (target) {
|
||
scrollEl.scrollTop += target.getBoundingClientRect().top - scrollEl.getBoundingClientRect().top;
|
||
}
|
||
}
|
||
|
||
container.querySelectorAll('.agenda-event').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const ev = events.find(ev => ev.id === el.dataset.id && ev.url === el.dataset.url);
|
||
if (ev) onEventClick(ev, el);
|
||
});
|
||
});
|
||
}
|
||
|
||
function isTodayDate(d) {
|
||
const now = new Date();
|
||
return d.getFullYear() === now.getFullYear() &&
|
||
d.getMonth() === now.getMonth() &&
|
||
d.getDate() === now.getDate();
|
||
}
|
||
|
||
function fmtTime(d) {
|
||
return d.toLocaleTimeString(getLocale(), { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
function escAttr(s) {
|
||
return String(s).replace(/"/g,'"').replace(/'/g,''');
|
||
}
|