feat(localization): expand language support and enhance UI for user settings
All checks were successful
Deploy to production / deploy (push) Successful in 3m0s

- Added support for additional UI locales including Cebuano and Spanish, improving accessibility for a broader user base.
- Updated language selection components in the AppHeader and SettingsWidget to reflect new language options, enhancing user experience.
- Enhanced localization of various UI elements across components, ensuring consistent language representation and improved user engagement.
- Implemented logic to synchronize user language preferences with backend settings, providing a seamless experience when changing languages.
This commit is contained in:
Torsten Schulz (local)
2026-04-02 07:54:44 +02:00
parent ac5d436a36
commit 6d9d69dc10
72 changed files with 1792 additions and 343 deletions

View File

@@ -2,10 +2,10 @@
<div class="home-logged-in">
<section class="dashboard-hero surface-card">
<div class="dashboard-hero__copy">
<span class="dashboard-kicker">Dein Bereich</span>
<h1>Willkommen zurück!</h1>
<span class="dashboard-kicker">{{ $t('home.dashboard.kicker') }}</span>
<h1>{{ $t('home.dashboard.title') }}</h1>
<p class="dashboard-subtitle">
Dein persönlicher Einstieg in Community, Termine, Falukant und laufende Aktivitäten.
{{ $t('home.dashboard.subtitle') }}
</p>
</div>
<div class="dashboard-toolbar surface-card">
@@ -15,7 +15,7 @@
class="btn-edit"
@click="editMode = true"
>
Dashboard bearbeiten
{{ $t('home.dashboard.edit') }}
</button>
<template v-else>
<div class="widget-add-row">
@@ -24,7 +24,7 @@
class="widget-type-select"
@change="onSelectWidgetType"
>
<option value="">+ Widget hinzufügen </option>
<option value="">{{ $t('home.dashboard.addWidget') }}</option>
<option
v-for="wt in widgetTypeOptions"
:key="wt.id"
@@ -39,11 +39,11 @@
class="btn-add-again"
@click="addSameWidgetType"
>
Nochmal hinzufügen
{{ $t('home.dashboard.addAgain') }}
</button>
</div>
<button type="button" class="btn-done" @click="doneEditing">
Fertig
{{ $t('home.dashboard.done') }}
</button>
</template>
</div>
@@ -51,19 +51,19 @@
<section class="dashboard-overview">
<article class="overview-card surface-card">
<span class="overview-card__label">Aktive Widgets</span>
<span class="overview-card__label">{{ $t('home.dashboard.overview.activeWidgetsLabel') }}</span>
<strong>{{ widgets.length }}</strong>
<p>Dein Dashboard ist modular aufgebaut und kann jederzeit umsortiert werden.</p>
<p>{{ $t('home.dashboard.overview.activeWidgetsText') }}</p>
</article>
<article class="overview-card surface-card">
<span class="overview-card__label">Verfügbare Module</span>
<span class="overview-card__label">{{ $t('home.dashboard.overview.availableModulesLabel') }}</span>
<strong>{{ widgetTypeOptions.length }}</strong>
<p>Du kannst Community-, Kalender-, News- und Falukant-Module kombinieren.</p>
<p>{{ $t('home.dashboard.overview.availableModulesText') }}</p>
</article>
<article class="overview-card surface-card">
<span class="overview-card__label">Bearbeitungsmodus</span>
<strong>{{ editMode ? 'Aktiv' : 'Aus' }}</strong>
<p>{{ editMode ? 'Widgets können gerade ergänzt und angepasst werden.' : 'Inhalte bleiben fokussiert und ruhig lesbar.' }}</p>
<span class="overview-card__label">{{ $t('home.dashboard.overview.editModeLabel') }}</span>
<strong>{{ editMode ? $t('home.dashboard.overview.editModeActive') : $t('home.dashboard.overview.editModeInactive') }}</strong>
<p>{{ editMode ? $t('home.dashboard.overview.editModeActiveText') : $t('home.dashboard.overview.editModeInactiveText') }}</p>
</article>
</section>
@@ -85,8 +85,8 @@
>
<div class="dashboard-shell__header">
<div>
<h2>Deine Übersicht</h2>
<p>Widgets lassen sich verschieben und im Bearbeitungsmodus anpassen.</p>
<h2>{{ $t('home.dashboard.sectionTitle') }}</h2>
<p>{{ $t('home.dashboard.sectionIntro') }}</p>
</div>
</div>
<div
@@ -120,17 +120,17 @@
<input
v-model="w.title"
type="text"
placeholder="Titel"
:placeholder="$t('home.dashboard.widgetTitlePlaceholder')"
class="widget-edit-input"
/>
</div>
<button
type="button"
class="btn-remove"
title="Widget entfernen"
:title="$t('home.dashboard.removeWidget')"
@click="removeWidget(index)"
>
Entfernen
{{ $t('home.dashboard.remove') }}
</button>
</div>
</div>
@@ -139,7 +139,7 @@
</div>
<div v-if="widgets.length === 0 && !loading" class="dashboard-empty">
<p>Noch keine Widgets. Klicke auf „Dashboard bearbeiten“ und dann „+ Widget hinzufügen“.</p>
<p>{{ $t('home.dashboard.empty') }}</p>
</div>
<div class="actions">
@@ -164,7 +164,7 @@ export default {
computed: {
widgetTypeOptions() {
if (this.availableWidgets.length > 0) return this.availableWidgets;
return [{ id: 'default', label: 'Termine', endpoint: '/api/termine' }];
return [{ id: 'default', label: this.$t('home.dashboard.defaultAppointmentsWidget'), endpoint: '/api/termine' }];
}
},
data() {
@@ -185,6 +185,41 @@ export default {
this.loadAvailableWidgets();
},
methods: {
getLocalizedWidgetLabel(endpoint, fallbackLabel = '') {
const key = {
'/api/termine': 'home.dashboard.widgetLabels.appointments',
'/api/falukant/dashboard-widget': 'home.dashboard.widgetLabels.falukant',
'/api/news': 'home.dashboard.widgetLabels.news',
'/api/calendar/widget/birthdays': 'home.dashboard.widgetLabels.birthdays',
'/api/calendar/widget/upcoming': 'home.dashboard.widgetLabels.upcoming',
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar'
}[endpoint];
return key ? this.$t(key) : fallbackLabel;
},
normalizeWidgetType(widgetType) {
return {
...widgetType,
label: this.getLocalizedWidgetLabel(widgetType?.endpoint, widgetType?.label || '')
};
},
normalizeWidgetConfig(widget) {
const localizedLabel = this.getLocalizedWidgetLabel(widget?.endpoint, '');
const title = String(widget?.title || '').trim();
const knownDefaultLabels = [
'Termine',
'Falukant',
'News',
'Geburtstage',
'Nächste Termine',
'Kalender'
];
return {
...widget,
title: !title || knownDefaultLabels.includes(title)
? (localizedLabel || title)
: title
};
},
/** Endpoint aus Widget-Typ (anhand gespeichertem endpoint gematcht), sonst w.endpoint. */
effectiveEndpoint(w) {
if (!w?.endpoint) return '';
@@ -204,7 +239,7 @@ export default {
async loadAvailableWidgets() {
try {
const { data } = await apiClient.get('/api/dashboard/widgets');
this.availableWidgets = Array.isArray(data) ? data : [];
this.availableWidgets = Array.isArray(data) ? data.map(widget => this.normalizeWidgetType(widget)) : [];
} catch (e) {
this.availableWidgets = [];
}
@@ -216,14 +251,14 @@ export default {
const { data } = await apiClient.get('/api/dashboard/config');
let list = Array.isArray(data?.widgets) ? [...data.widgets] : [];
if (list.length === 0) {
list = [{ id: generateId(), title: 'Termine', endpoint: '/api/termine' }];
list = [{ id: generateId(), title: this.$t('home.dashboard.defaultAppointmentsWidget'), endpoint: '/api/termine' }];
this.widgets = list;
await this.saveConfig();
} else {
this.widgets = list;
this.widgets = list.map(widget => this.normalizeWidgetConfig(widget));
}
} catch (e) {
this.loadError = e.response?.data?.error || e.message || 'Dashboard konnte nicht geladen werden.';
this.loadError = e.response?.data?.error || e.message || this.$t('home.dashboard.loadError');
} finally {
this.loading = false;
}
@@ -239,7 +274,7 @@ export default {
await apiClient.put('/api/dashboard/config', { widgets: payload });
} catch (e) {
console.error('Dashboard speichern fehlgeschlagen:', e);
this.saveError = e.response?.data?.error || e.message || 'Dashboard konnte nicht gespeichert werden.';
this.saveError = e.response?.data?.error || e.message || this.$t('home.dashboard.saveError');
}
},
addWidgetFromType(wt) {