feat: i18n notes, contacts, budget, settings pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-03-31 22:57:45 +02:00
parent e6c6b0a4fc
commit 26bbd61e1d
4 changed files with 206 additions and 185 deletions
+36 -35
View File
@@ -7,6 +7,7 @@
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal, btnError } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
// --------------------------------------------------------
// Konstanten
@@ -48,20 +49,20 @@ export async function render(container, { user }) {
container.innerHTML = `
<div class="notes-page">
<div class="notes-toolbar">
<h1 class="notes-toolbar__title">Pinnwand</h1>
<h1 class="notes-toolbar__title">${t('notes.title')}</h1>
<div class="notes-toolbar__search">
<i data-lucide="search" class="notes-toolbar__search-icon" aria-hidden="true"></i>
<input type="search" id="notes-search" class="notes-toolbar__search-input"
placeholder="Notizen durchsuchen…" autocomplete="off"
placeholder="${t('notes.searchPlaceholder')}" autocomplete="off"
value="${escHtml(state.filterQuery)}">
</div>
<button class="btn btn--primary" id="notes-add-btn">
<i data-lucide="plus" style="width:16px;height:16px;margin-right:4px;" aria-hidden="true"></i>
Neue Notiz
${t('notes.addNoteLabel')}
</button>
</div>
<div id="notes-grid" class="notes-grid"></div>
<button class="page-fab" id="fab-new-note" aria-label="Neue Notiz">
<button class="page-fab" id="fab-new-note" aria-label="${t('notes.addNoteLabel')}">
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
</button>
</div>
@@ -75,7 +76,7 @@ export async function render(container, { user }) {
} catch (err) {
console.error('[Notes] Laden fehlgeschlagen:', err);
state.notes = [];
window.oikos?.showToast('Notizen konnten nicht geladen werden.', 'danger');
window.oikos?.showToast(t('notes.loadError'), 'danger');
}
const grid = container.querySelector('#notes-grid');
grid.addEventListener('click', async (e) => {
@@ -131,8 +132,8 @@ function renderGrid() {
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
<div class="empty-state__title">${isFiltered ? 'Keine Treffer' : 'Noch keine Notizen'}</div>
<div class="empty-state__description">${isFiltered ? `Keine Notiz enthält „${escHtml(state.filterQuery)}".` : 'Neue Notiz über den + Button erstellen.'}</div>
<div class="empty-state__title">${isFiltered ? t('notes.noResultsTitle') : t('notes.emptyTitle')}</div>
<div class="empty-state__description">${isFiltered ? t('notes.noResultsDescription', { query: state.filterQuery }) : t('notes.emptyDescription')}</div>
</div>
`;
if (window.lucide) lucide.createIcons();
@@ -156,7 +157,7 @@ function renderNoteCard(note) {
data-id="${note.id}"
style="background-color:${escHtml(note.color)};color:${textColor};">
<button class="note-card__pin" data-action="pin" data-id="${note.id}"
aria-label="${note.pinned ? 'Anpinnen aufheben' : 'Anpinnen'}">
aria-label="${note.pinned ? t('notes.unpinAction') : t('notes.pinAction')}">
<i data-lucide="${note.pinned ? 'pin-off' : 'pin'}" style="width:12px;height:12px;" aria-hidden="true"></i>
</button>
${note.title ? `<div class="note-card__title">${escHtml(note.title)}</div>` : ''}
@@ -167,7 +168,7 @@ function renderNoteCard(note) {
style="background-color:${escHtml(note.creator_color || '#8E8E93')}">${initials}</span>
<span>${escHtml(note.creator_name || '')}</span>
</div>
<button class="note-card__delete" data-action="delete" data-id="${note.id}" aria-label="Notiz löschen">
<button class="note-card__delete" data-action="delete" data-id="${note.id}" aria-label="${t('notes.deleteLabel')}">
<i data-lucide="trash-2" style="width:12px;height:12px;" aria-hidden="true"></i>
</button>
</div>
@@ -315,58 +316,58 @@ function openNoteModal({ mode, note = null }) {
const content = `
<div class="form-group">
<label class="form-label" for="note-title">Titel (optional)</label>
<label class="form-label" for="note-title">${t('notes.titleLabel')}</label>
<input type="text" class="form-input" id="note-title"
placeholder="Kein Titel" value="${escHtml(isEdit && note.title ? note.title : '')}">
placeholder="${t('notes.titlePlaceholder')}" value="${escHtml(isEdit && note.title ? note.title : '')}">
</div>
<div class="form-group">
<label class="form-label" for="note-content">Inhalt <span style="font-weight:400;color:var(--text-tertiary);font-size:.85em;">(Markdown-Formatierung möglich)</span></label>
<label class="form-label" for="note-content">${t('notes.contentLabel')} <span style="font-weight:400;color:var(--text-tertiary);font-size:.85em;">${t('notes.contentMarkdownHint')}</span></label>
<div class="note-format-toolbar">
<button type="button" class="note-format-btn" data-format="bold" title="Fett (Strg+B)">
<button type="button" class="note-format-btn" data-format="bold" title="${t('notes.formatBold')}">
<i data-lucide="bold" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="italic" title="Kursiv (Strg+I)">
<button type="button" class="note-format-btn" data-format="italic" title="${t('notes.formatItalic')}">
<i data-lucide="italic" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="underline" title="Unterstrichen (Strg+U)">
<button type="button" class="note-format-btn" data-format="underline" title="${t('notes.formatUnderline')}">
<i data-lucide="underline" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="strikethrough" title="Durchgestrichen">
<button type="button" class="note-format-btn" data-format="strikethrough" title="${t('notes.formatStrikethrough')}">
<i data-lucide="strikethrough" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<span class="note-format-btn--sep"></span>
<button type="button" class="note-format-btn" data-format="heading" title="Überschrift">
<button type="button" class="note-format-btn" data-format="heading" title="${t('notes.formatHeading')}">
<i data-lucide="heading" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="list" title="Aufzählung">
<button type="button" class="note-format-btn" data-format="list" title="${t('notes.formatList')}">
<i data-lucide="list" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="ordered-list" title="Nummerierte Liste">
<button type="button" class="note-format-btn" data-format="ordered-list" title="${t('notes.formatOrderedList')}">
<i data-lucide="list-ordered" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="checklist" title="Checkliste">
<button type="button" class="note-format-btn" data-format="checklist" title="${t('notes.formatChecklist')}">
<i data-lucide="list-checks" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<span class="note-format-btn--sep"></span>
<button type="button" class="note-format-btn" data-format="link" title="Link">
<button type="button" class="note-format-btn" data-format="link" title="${t('notes.formatLink')}">
<i data-lucide="link" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="code" title="Code">
<button type="button" class="note-format-btn" data-format="code" title="${t('notes.formatCode')}">
<i data-lucide="code" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="quote" title="Zitat">
<button type="button" class="note-format-btn" data-format="quote" title="${t('notes.formatQuote')}">
<i data-lucide="quote" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
<button type="button" class="note-format-btn" data-format="divider" title="Trennlinie">
<button type="button" class="note-format-btn" data-format="divider" title="${t('notes.formatDivider')}">
<i data-lucide="minus" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
</div>
<textarea class="form-input" id="note-content" rows="6"
placeholder="Notiz eingeben…"
placeholder="${t('notes.contentPlaceholder')}"
style="resize:vertical;">${escHtml(isEdit ? note.content : '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Farbe</label>
<label class="form-label">${t('notes.colorLabel')}</label>
<div class="note-color-picker">
${NOTE_COLORS.map((c) => `
<div class="note-color-swatch ${c === selColor ? 'note-color-swatch--active' : ''}"
@@ -379,17 +380,17 @@ function openNoteModal({ mode, note = null }) {
<div class="form-group">
<label class="allday-toggle">
<input type="checkbox" id="note-pinned" ${isEdit && note.pinned ? 'checked' : ''}>
<span class="allday-toggle__label">Anpinnen (erscheint auf Dashboard)</span>
<span class="allday-toggle__label">${t('notes.pinnedLabel')}</span>
</label>
</div>
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
<button class="btn btn--secondary" id="note-modal-cancel">Abbrechen</button>
<button class="btn btn--primary" id="note-modal-save">${isEdit ? 'Speichern' : 'Erstellen'}</button>
<button class="btn btn--secondary" id="note-modal-cancel">${t('common.cancel')}</button>
<button class="btn btn--primary" id="note-modal-save">${isEdit ? t('common.save') : t('common.create')}</button>
</div>`;
openSharedModal({
title: isEdit ? 'Notiz bearbeiten' : 'Neue Notiz',
title: isEdit ? t('notes.editNote') : t('notes.newNote'),
content,
size: 'md',
onSave(panel) {
@@ -427,7 +428,7 @@ function openNoteModal({ mode, note = null }) {
const color = panel.querySelector('.note-color-swatch--active')?.dataset.color || NOTE_COLORS[0];
const pinned = panel.querySelector('#note-pinned').checked ? 1 : 0;
if (!cnt) { window.oikos?.showToast('Inhalt ist erforderlich', 'error'); return; }
if (!cnt) { window.oikos?.showToast(t('common.contentRequired'), 'error'); return; }
saveBtn.disabled = true;
saveBtn.textContent = '…';
@@ -444,12 +445,12 @@ function openNoteModal({ mode, note = null }) {
}
closeModal();
renderGrid();
window.oikos?.showToast(mode === 'create' ? 'Notiz erstellt' : 'Notiz gespeichert', 'success');
window.oikos?.showToast(mode === 'create' ? t('notes.createdToast') : t('notes.savedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
btnError(saveBtn);
saveBtn.disabled = false;
saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen';
saveBtn.textContent = isEdit ? t('common.save') : t('common.create');
}
});
},
@@ -473,13 +474,13 @@ async function togglePin(id) {
}
async function deleteNote(id) {
if (!confirm('Notiz wirklich löschen?')) return;
if (!confirm(t('notes.deleteConfirm'))) return;
try {
await api.delete(`/notes/${id}`);
state.notes = state.notes.filter((n) => n.id !== id);
renderGrid();
vibrate([30, 50, 30]);
window.oikos?.showToast('Notiz gelöscht', 'success');
window.oikos?.showToast(t('notes.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
}