diff --git a/public/pages/budget.js b/public/pages/budget.js
index 301a209..bdfa7a2 100644
--- a/public/pages/budget.js
+++ b/public/pages/budget.js
@@ -8,6 +8,7 @@
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
+import { t } from '/i18n.js';
// --------------------------------------------------------
// Konstanten
@@ -18,6 +19,18 @@ const CATEGORIES = [
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
];
+const CATEGORY_LABELS = () => ({
+ 'Lebensmittel': t('budget.catFood'),
+ 'Miete': t('budget.catRent'),
+ 'Versicherung': t('budget.catInsurance'),
+ 'Mobilität': t('budget.catMobility'),
+ 'Freizeit': t('budget.catLeisure'),
+ 'Kleidung': t('budget.catClothing'),
+ 'Gesundheit': t('budget.catHealth'),
+ 'Bildung': t('budget.catEducation'),
+ 'Sonstiges': t('budget.catMisc'),
+});
+
const MONTH_NAMES = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
@@ -74,7 +87,7 @@ async function loadMonth(month) {
state.entries = [];
state.summary = { income: 0, expenses: 0, balance: 0, byCategory: [] };
state.prevSummary = null;
- window.oikos?.showToast('Budget konnte nicht geladen werden.', 'danger');
+ window.oikos?.showToast(t('budget.loadError'), 'danger');
}
}
@@ -89,24 +102,24 @@ export async function render(container, { user }) {
container.innerHTML = `
-
Budget
+
${t('budget.title')}
-
-
Lade…
+
${t('budget.loadingIndicator')}
-
+
@@ -171,17 +184,17 @@ function renderBody() {
-
Einnahmen
+
${t('budget.income')}
${formatAmount(s.income)}
${p ? renderTrend(s.income, p.income, prevLabel) : ''}
-
Ausgaben
+
${t('budget.expenses')}
${formatAmount(Math.abs(s.expenses))}
${p ? renderTrend(s.expenses, p.expenses, prevLabel) : ''}
-
Saldo
+
${t('budget.balance')}
${formatAmount(s.balance)}
${p ? renderTrend(s.balance, p.balance, prevLabel) : ''}
@@ -190,7 +203,7 @@ function renderBody() {
${s.byCategory.length ? `
-
Nach Kategorie
+
${t('budget.byCategory')}
${renderCategoryBars(s.byCategory)}
@@ -199,7 +212,7 @@ function renderBody() {
`;
}
@@ -277,7 +290,7 @@ function renderEntries() {
${date} · ${escHtml(e.category)}${recurTag}
${sign}${formatAmount(e.amount)}
-
+
@@ -298,7 +311,7 @@ function renderEntries() {
function renderTrend(current, prev, prevLabel) {
const delta = current - prev;
if (Math.abs(delta) < 0.005) {
- return `
— wie ${prevLabel}
`;
+ return `
${t('budget.trendNeutral', { month: prevLabel })}
`;
}
const positive = delta > 0;
const arrow = positive ? '▲' : '▼';
@@ -323,38 +336,39 @@ function openBudgetModal({ mode, entry = null }) {
const isExpense = isEdit ? entry.amount < 0 : true;
const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : '';
+ const catLabels = CATEGORY_LABELS();
const catOpts = CATEGORIES.map((c) =>
- `
`
+ `
`
).join('');
const content = `
Ausgabe
+ id="type-expense" type="button">${t('budget.typeExpense')}
Einnahme
+ id="type-income" type="button">${t('budget.typeIncome')}
-
+
+ placeholder="${t('budget.titlePlaceholder')}" value="${escHtml(isEdit ? entry.title : '')}">
-
+
-
+
-
+
@@ -362,22 +376,22 @@ function openBudgetModal({ mode, entry = null }) {
`;
openSharedModal({
- title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag',
+ title: isEdit ? t('budget.editEntry') : t('budget.newEntry'),
content,
size: 'sm',
onSave(panel) {
@@ -397,7 +411,7 @@ function openBudgetModal({ mode, entry = null }) {
panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
panel.querySelector('#bm-delete')?.addEventListener('click', async () => {
- if (!confirm(`"${entry.title}" wirklich löschen?`)) return;
+ if (!confirm(t('budget.deletePersonConfirm', { title: entry.title }))) return;
closeModal();
await deleteEntry(entry.id);
});
@@ -410,9 +424,9 @@ function openBudgetModal({ mode, entry = null }) {
const date = panel.querySelector('#bm-date').value;
const recurring = panel.querySelector('#bm-recurring').checked ? 1 : 0;
- if (!title) { window.oikos?.showToast('Titel ist erforderlich', 'error'); return; }
- if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast('Gültigen Betrag eingeben', 'error'); return; }
- if (!date) { window.oikos?.showToast('Datum ist erforderlich', 'error'); return; }
+ if (!title) { window.oikos?.showToast(t('common.titleRequired'), 'error'); return; }
+ if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast(t('budget.validAmountRequired'), 'error'); return; }
+ if (!date) { window.oikos?.showToast(t('budget.dateRequired'), 'error'); return; }
const amount = currentType === 'expense' ? -absVal : absVal;
@@ -434,11 +448,11 @@ function openBudgetModal({ mode, entry = null }) {
closeModal();
renderBody();
- window.oikos?.showToast(mode === 'create' ? 'Eintrag hinzugefügt' : 'Eintrag gespeichert', 'success');
+ window.oikos?.showToast(mode === 'create' ? t('budget.addedToast') : t('budget.savedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
saveBtn.disabled = false;
- saveBtn.textContent = isEdit ? 'Speichern' : 'Hinzufügen';
+ saveBtn.textContent = isEdit ? t('common.save') : t('common.add');
}
});
},
@@ -450,7 +464,7 @@ function openBudgetModal({ mode, entry = null }) {
// --------------------------------------------------------
async function deleteEntry(id) {
- if (!confirm('Eintrag wirklich löschen?')) return;
+ if (!confirm(t('budget.deleteConfirm'))) return;
try {
await api.delete(`/budget/${id}`);
state.entries = state.entries.filter((e) => e.id !== id);
@@ -458,7 +472,7 @@ async function deleteEntry(id) {
state.summary = sumRes.data;
renderBody();
vibrate([30, 50, 30]);
- window.oikos?.showToast('Eintrag gelöscht', 'success');
+ window.oikos?.showToast(t('budget.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
}
diff --git a/public/pages/contacts.js b/public/pages/contacts.js
index a19b225..57d2b3c 100644
--- a/public/pages/contacts.js
+++ b/public/pages/contacts.js
@@ -7,6 +7,7 @@
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
+import { t } from '/i18n.js';
// --------------------------------------------------------
// Konstanten
@@ -44,32 +45,32 @@ export async function render(container, { user }) {
_container = container;
container.innerHTML = `
-
Kontakte
+
${t('contacts.title')}
- Alle
+ ${t('contacts.filterAll')}
${CATEGORIES.map((c) => `
${CATEGORY_ICONS[c] || ''} ${escHtml(c)}
`).join('')}
-
+
@@ -115,13 +116,13 @@ export async function render(container, { user }) {
try {
const text = await file.text();
const contact = parseVCard(text);
- if (!contact.name) { window.oikos?.showToast('vCard enthält keinen Namen.', 'warning'); return; }
+ if (!contact.name) { window.oikos?.showToast(t('contacts.vcardNoName'), 'warning'); return; }
const res = await api.post('/contacts', contact);
state.contacts.push(res.data);
renderList();
- window.oikos?.showToast(`${res.data.name} importiert.`, 'success');
+ window.oikos?.showToast(t('contacts.importedToast', { name: res.data.name }), 'success');
} catch (err) {
- window.oikos?.showToast('Import fehlgeschlagen: ' + err.message, 'danger');
+ window.oikos?.showToast(t('contacts.importError', { error: err.message }), 'danger');
}
});
}
@@ -164,8 +165,8 @@ function renderList() {
-
Noch keine Kontakte
-
Neue Kontakte über den + Button hinzufügen.
+
${t('contacts.emptyTitle')}
+
${t('contacts.emptyDescription')}
`;
if (window.lucide) lucide.createIcons();
@@ -207,9 +208,9 @@ function renderList() {
}
function renderContactItem(c) {
- const phone = c.phone ? `${isFiltered ? `Keine Notiz enthält „${escHtml(state.filterQuery)}".` : 'Neue Notiz über den + Button erstellen.'}
+ ${isFiltered ? t('notes.noResultsDescription', { query: state.filterQuery }) : t('notes.emptyDescription')}
`;
if (window.lucide) lucide.createIcons();
@@ -156,7 +157,7 @@ function renderNoteCard(note) {
data-id="${note.id}"
style="background-color:${escHtml(note.color)};color:${textColor};">