@@ -584,12 +597,12 @@ function renderAgendaView(container) {
container.innerHTML = `
${groups.length === 0
- ? `
Keine Termine im gewählten Zeitraum.
`
+ ? `
${t('calendar.noEvents')}
`
: groups.map(({ date, events }) => `
${events.map((ev) => renderAgendaEvent(ev)).join('')}
@@ -611,9 +624,9 @@ function renderAgendaView(container) {
function renderAgendaEvent(ev) {
const timeStr = ev.all_day
- ? 'Ganztägig'
+ ? t('calendar.allDay')
: formatTime(ev.start_datetime)
- + (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : ' Uhr');
+ + (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} ${t('calendar.timeSuffix')}`.trimEnd() : ` ${t('calendar.timeSuffix')}`.trimEnd());
const initials = ev.assigned_name
? ev.assigned_name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2)
@@ -650,9 +663,9 @@ function showEventPopup(ev, anchor) {
popup.className = 'event-popup';
const timeStr = ev.all_day
- ? 'Ganztägig'
+ ? t('calendar.allDay')
: formatDateTime(ev.start_datetime)
- + (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : '');
+ + (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)}${t('calendar.timeSuffix') ? ' ' + t('calendar.timeSuffix') : ''}`.trim() : '');
popup.innerHTML = `
@@ -664,7 +677,7 @@ function showEventPopup(ev, anchor) {
${ev.assigned_name ? `
👤 ${escHtml(ev.assigned_name)}
` : ''}
`;
if (window.lucide) lucide.createIcons();
@@ -183,7 +196,7 @@ function renderList() {
.sort(([a], [b]) => CATEGORIES.indexOf(a) - CATEGORIES.indexOf(b))
.map(([cat, items]) => `
-
+
${items.map((c) => renderContactItem(c)).join('')}
`).join('');
@@ -207,9 +220,9 @@ function renderList() {
}
function renderContactItem(c) {
- const phone = c.phone ? `
` : '';
- const email = c.email ? `
` : '';
- const maps = c.address ? `
` : '';
+ const phone = c.phone ? `
` : '';
+ const email = c.email ? `
` : '';
+ const maps = c.address ? `
` : '';
const meta = [c.phone, c.email].filter(Boolean).join(' · ');
return `
@@ -222,10 +235,10 @@ function renderContactItem(c) {
@@ -241,55 +254,56 @@ function openContactModal({ mode, contact = null }) {
const isEdit = mode === 'edit';
const v = (field) => escHtml(isEdit && contact[field] ? contact[field] : '');
+ const catLabels = CATEGORY_LABELS();
const catOpts = CATEGORIES.map((c) =>
- `
`
+ `
`
).join('');
const content = `
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
`;
openSharedModal({
- title: isEdit ? 'Kontakt bearbeiten' : 'Neuer Kontakt',
+ title: isEdit ? t('contacts.editContact') : t('contacts.newContact'),
content,
size: 'md',
onSave(panel) {
panel.querySelector('#cm-cancel').addEventListener('click', closeModal);
panel.querySelector('#cm-delete')?.addEventListener('click', async () => {
- if (!confirm(`"${contact.name}" wirklich löschen?`)) return;
+ if (!confirm(t('contacts.deletePersonConfirm', { name: contact.name }))) return;
closeModal();
await deleteContact(contact.id);
});
@@ -303,7 +317,7 @@ function openContactModal({ mode, contact = null }) {
const address = panel.querySelector('#cm-address').value.trim() || null;
const notes = panel.querySelector('#cm-notes').value.trim() || null;
- if (!name) { window.oikos?.showToast('Name ist erforderlich', 'error'); return; }
+ if (!name) { window.oikos?.showToast(t('common.nameRequired'), 'error'); return; }
saveBtn.disabled = true;
saveBtn.textContent = '…';
@@ -324,11 +338,11 @@ function openContactModal({ mode, contact = null }) {
}
closeModal();
renderList();
- window.oikos?.showToast(mode === 'create' ? 'Kontakt gespeichert' : 'Kontakt aktualisiert', 'success');
+ window.oikos?.showToast(mode === 'create' ? t('contacts.savedToast') : t('contacts.updatedToast'), 'success');
} catch (err) {
- window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
+ window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
saveBtn.disabled = false;
- saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen';
+ saveBtn.textContent = isEdit ? t('common.save') : t('common.create');
}
});
},
@@ -336,15 +350,15 @@ function openContactModal({ mode, contact = null }) {
}
async function deleteContact(id) {
- if (!confirm('Kontakt wirklich löschen?')) return;
+ if (!confirm(t('contacts.deleteConfirm'))) return;
try {
await api.delete(`/contacts/${id}`);
state.contacts = state.contacts.filter((c) => c.id !== id);
renderList();
vibrate([30, 50, 30]);
- window.oikos?.showToast('Kontakt gelöscht', 'success');
+ window.oikos?.showToast(t('contacts.deletedToast'), 'success');
} catch (err) {
- window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
+ window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
}
}
diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js
index ba0aaae..e43655a 100644
--- a/public/pages/dashboard.js
+++ b/public/pages/dashboard.js
@@ -5,6 +5,7 @@
*/
import { api } from '/api.js';
+import { t, formatDate, formatTime, getLocale } from '/i18n.js';
// Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert.
let _fabController = null;
@@ -15,14 +16,9 @@ let _fabController = null;
function greeting(displayName) {
const h = new Date().getHours();
- const tageszeit = h < 12 ? 'Morgen' : h < 18 ? 'Tag' : 'Abend';
- return `Guten ${tageszeit}, ${displayName}`;
-}
-
-function formatDate(date = new Date()) {
- return date.toLocaleDateString('de-DE', {
- weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
- });
+ if (h < 12) return t('dashboard.greetingMorning', { name: displayName });
+ if (h < 18) return t('dashboard.greetingDay', { name: displayName });
+ return t('dashboard.greetingEvening', { name: displayName });
}
function formatDateTime(isoString) {
@@ -33,13 +29,14 @@ function formatDateTime(isoString) {
tomorrow.setDate(today.getDate() + 1);
const dateStr = d.toDateString() === today.toDateString()
- ? 'Heute'
+ ? t('common.today')
: d.toDateString() === tomorrow.toDateString()
- ? 'Morgen'
- : d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
+ ? t('common.tomorrow')
+ : formatDate(d);
- const timeStr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
- return `${dateStr}, ${timeStr} Uhr`;
+ const timeStr = formatTime(d);
+ const suffix = t('calendar.timeSuffix');
+ return `${dateStr}, ${timeStr}${suffix ? ' ' + suffix : ''}`.trim();
}
function formatDueDate(dateStr) {
@@ -49,21 +46,21 @@ function formatDueDate(dateStr) {
const diffMs = due - now;
const diffH = diffMs / (1000 * 60 * 60);
- if (diffMs < 0) return { text: 'Überfällig', overdue: true };
- if (diffH < 24) return { text: 'Heute fällig', overdue: false };
- if (diffH < 48) return { text: 'Morgen fällig', overdue: false };
+ if (diffMs < 0) return { text: t('dashboard.overdue'), overdue: true };
+ if (diffH < 24) return { text: t('dashboard.dueSoon'), overdue: false };
+ if (diffH < 48) return { text: t('dashboard.dueTomorrow'), overdue: false };
return {
- text: due.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }),
+ text: formatDate(due),
overdue: false,
};
}
-const MEAL_LABELS = {
- breakfast: 'Frühstück',
- lunch: 'Mittagessen',
- dinner: 'Abendessen',
- snack: 'Snack',
-};
+const MEAL_LABELS = () => ({
+ breakfast: t('meals.typeBreakfast'),
+ lunch: t('meals.typeLunch'),
+ dinner: t('meals.typeDinner'),
+ snack: t('meals.typeSnack'),
+});
const MEAL_ICONS = {
breakfast: 'sunrise',
@@ -76,7 +73,8 @@ function initials(name = '') {
return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase();
}
-function widgetHeader(icon, title, count, linkHref, linkLabel = 'Alle') {
+function widgetHeader(icon, title, count, linkHref, linkLabel) {
+ linkLabel = linkLabel ?? t('dashboard.allLink');
const badge = count != null
? `
${count}`
: '';
@@ -122,24 +120,24 @@ function renderGreeting(user, stats = {}) {
if (urgentCount > 0)
statChips.push(`
- ${urgentCount} dring. Aufgabe${urgentCount > 1 ? 'n' : ''}
+ ${urgentCount > 1 ? t('dashboard.urgentTasksChipPlural', { count: urgentCount }) : t('dashboard.urgentTasksChip', { count: urgentCount })}
`);
if (todayEventCount > 0)
statChips.push(`
- ${todayEventCount} Termin${todayEventCount > 1 ? 'e' : ''} heute
+ ${todayEventCount > 1 ? t('dashboard.eventsChipPlural', { count: todayEventCount }) : t('dashboard.eventsChip', { count: todayEventCount })}
`);
if (todayMealTitle)
statChips.push(`
- Heute: ${todayMealTitle}
+ ${t('dashboard.todayMealChip', { title: todayMealTitle })}
`);
return `
@@ -149,10 +147,10 @@ function renderGreeting(user, stats = {}) {
function renderUrgentTasks(tasks) {
if (!tasks.length) {
return `
`;
}
@@ -174,7 +172,7 @@ function renderUrgentTasks(tasks) {
}).join('');
return `
`;
}
@@ -182,10 +180,10 @@ function renderUrgentTasks(tasks) {
function renderUpcomingEvents(events) {
if (!events.length) {
return `
`;
}
@@ -194,14 +192,15 @@ function renderUpcomingEvents(events) {
const items = events.map((e) => {
const d = new Date(e.start_datetime);
const isToday = d.toDateString() === today;
- const timeStr = e.all_day ? 'Ganztägig' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
+ const _suffix = t('calendar.timeSuffix');
+ const timeStr = e.all_day ? t('dashboard.allDay') : `${formatTime(d)}${_suffix ? ' ' + _suffix : ''}`.trim();
return `
${e.title}
- ${isToday ? 'Heute' : formatDateTime(e.start_datetime).split(',')[0]}
+ ${isToday ? t('common.today') : formatDateTime(e.start_datetime).split(',')[0]}
${timeStr}
${e.location ? ` · ${e.location}` : ''}
@@ -211,7 +210,7 @@ function renderUpcomingEvents(events) {
}).join('');
return `
`;
}
@@ -219,19 +218,20 @@ function renderUpcomingEvents(events) {
function renderTodayMeals(meals) {
const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack'];
+ const mealLabels = MEAL_LABELS();
const slots = MEAL_ORDER.map((type) => {
const meal = meals.find((m) => m.meal_type === type);
return `
-
${MEAL_LABELS[type]}
+
${mealLabels[type]}
${meal ? meal.title : '—'}
`;
}).join('');
return `
`;
}
@@ -239,10 +239,10 @@ function renderTodayMeals(meals) {
function renderPinnedNotes(notes) {
if (!notes.length) {
return `
`;
}
@@ -256,7 +256,7 @@ function renderPinnedNotes(notes) {
`).join('');
return `
`;
}
@@ -274,7 +274,7 @@ function renderWeatherWidget(weather) {
const forecastHtml = forecast.map((d, i) => {
const date = new Date(d.date + 'T12:00:00');
- const label = date.toLocaleDateString('de-DE', { weekday: 'short' });
+ const label = new Intl.DateTimeFormat(getLocale(), { weekday: 'short' }).format(date);
const extraCls = i >= 3 ? ' weather-forecast__day--extended' : '';
return `
`;
@@ -200,8 +219,8 @@ function renderTaskGroups(tasks, groupMode) {
-
Keine Aufgaben — alles erledigt?
-
Neue Aufgaben über den + Button erstellen.
+
${t('tasks.emptyTitle')}
+
${t('tasks.emptyDescription')}
`;
}
@@ -227,11 +246,12 @@ function renderModalContent({ task = null, users = [] } = {}) {
`
`
).join('');
+ const catLabels = CATEGORY_LABELS();
const categoryOptions = CATEGORIES.map((c) =>
- `
`
+ `
`
).join('');
- const priorityOptions = PRIORITIES.map((p) =>
+ const priorityOptions = PRIORITIES().map((p) =>
`
`
).join('');
@@ -241,36 +261,36 @@ function renderModalContent({ task = null, users = [] } = {}) {
-
+
-
+
-
+
@@ -279,30 +299,30 @@ function renderModalContent({ task = null, users = [] } = {}) {
-
+
${isEdit ? `
-
+
@@ -315,9 +335,9 @@ function renderModalContent({ task = null, users = [] } = {}) {
`;
@@ -375,7 +395,7 @@ async function loadTaskForEdit(id) {
function openTaskModal({ task = null, users = [] } = {}, container) {
const isEdit = !!task;
openSharedModal({
- title: isEdit ? 'Aufgabe bearbeiten' : 'Neue Aufgabe',
+ title: isEdit ? t('tasks.editTask') : t('tasks.newTask'),
content: renderModalContent({ task, users }),
size: 'lg',
onSave(panel) {
@@ -408,9 +428,9 @@ async function handleFormSubmit(e, container) {
errorEl.hidden = true;
submitBtn.disabled = true;
- submitBtn.textContent = 'Wird gespeichert…';
+ submitBtn.textContent = t('common.saving');
- const originalLabel = taskId ? 'Speichern' : 'Erstellen';
+ const originalLabel = taskId ? t('common.save') : t('common.create');
const rrule = getRRuleValues(document, 'task');
const body = {
@@ -429,10 +449,10 @@ async function handleFormSubmit(e, container) {
try {
if (taskId) {
await api.put(`/tasks/${taskId}`, body);
- window.oikos.showToast('Aufgabe gespeichert.', 'success');
+ window.oikos.showToast(t('tasks.savedToast'), 'success');
} else {
await api.post('/tasks', body);
- window.oikos.showToast('Aufgabe erstellt.', 'success');
+ window.oikos.showToast(t('tasks.createdToast'), 'success');
}
btnSuccess(submitBtn, originalLabel);
setTimeout(() => closeModal(), 700);
@@ -447,11 +467,11 @@ async function handleFormSubmit(e, container) {
}
async function handleDeleteTask(id, container) {
- if (!confirm('Aufgabe und alle Teilaufgaben löschen?')) return;
+ if (!confirm(t('tasks.deleteConfirm'))) return;
try {
await api.delete(`/tasks/${id}`);
closeModal();
- window.oikos.showToast('Aufgabe gelöscht.', 'default');
+ window.oikos.showToast(t('tasks.deletedToast'), 'default');
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
@@ -459,7 +479,7 @@ async function handleDeleteTask(id, container) {
}
async function handleAddSubtask(parentId, container) {
- const title = prompt('Teilaufgabe:');
+ const title = prompt(t('tasks.subtaskPrompt'));
if (!title?.trim()) return;
try {
await api.post('/tasks', { title: title.trim(), parent_task_id: parentId });
@@ -473,10 +493,10 @@ async function handleAddSubtask(parentId, container) {
// Kanban-Ansicht
// --------------------------------------------------------
-const KANBAN_COLS = [
- { status: 'open', label: 'Offen', colorVar: '--color-text-secondary' },
- { status: 'in_progress', label: 'In Bearbeitung', colorVar: '--color-warning' },
- { status: 'done', label: 'Erledigt', colorVar: '--color-success' },
+const KANBAN_COLS = () => [
+ { status: 'open', label: t('tasks.kanbanOpen'), colorVar: '--color-text-secondary' },
+ { status: 'in_progress', label: t('tasks.kanbanInProgress'), colorVar: '--color-warning' },
+ { status: 'done', label: t('tasks.kanbanDone'), colorVar: '--color-success' },
];
function renderKanbanCard(task) {
@@ -503,8 +523,9 @@ function renderKanban(container) {
const listEl = container.querySelector('#task-list');
if (!listEl) return;
+ const cols = KANBAN_COLS();
const grouped = {};
- for (const col of KANBAN_COLS) grouped[col.status] = [];
+ for (const col of cols) grouped[col.status] = [];
for (const t of state.tasks) {
if (grouped[t.status]) grouped[t.status].push(t);
else grouped['open'].push(t);
@@ -512,7 +533,7 @@ function renderKanban(container) {
listEl.innerHTML = `
- ${KANBAN_COLS.map((col) => `
+ ${cols.map((col) => `
-
@@ -981,7 +1004,7 @@ export async function render(container, { user }) {
state.users = metaData.users ?? [];
} catch (err) {
console.error('[Tasks] Ladefehler:', err.message);
- window.oikos.showToast('Aufgaben konnten nicht geladen werden.', 'danger');
+ window.oikos.showToast(t('tasks.loadError'), 'danger');
state.tasks = [];
state.users = [];
}
diff --git a/public/router.js b/public/router.js
index e4e1745..5f21124 100644
--- a/public/router.js
+++ b/public/router.js
@@ -5,6 +5,7 @@
*/
import { auth } from '/api.js';
+import { initI18n, getLocale, t } from '/i18n.js';
// --------------------------------------------------------
// Routen-Definitionen
@@ -215,8 +216,8 @@ async function renderPage(route, previousPath = null) {
*/
function renderAppShell(container) {
container.innerHTML = `
-
Zum Inhalt springen
-