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

@@ -9,19 +9,19 @@
<div class="birthday-info">
<div class="birthday-name">{{ birthday.username }}</div>
<div class="birthday-date">
<span v-if="birthday.daysUntil === 0" class="birthday-highlight">Heute!</span>
<span v-else-if="birthday.daysUntil === 1">Morgen</span>
<span v-if="birthday.daysUntil === 0" class="birthday-highlight">{{ $t('widgets.birthdays.today') }}</span>
<span v-else-if="birthday.daysUntil === 1">{{ $t('widgets.birthdays.tomorrow') }}</span>
<span v-else>{{ formatDate(birthday.nextDate) }}</span>
<span class="birthday-age">(wird {{ birthday.turningAge }})</span>
<span class="birthday-age">{{ $t('widgets.birthdays.turningAge', { age: birthday.turningAge }) }}</span>
</div>
</div>
<div v-if="birthday.daysUntil > 1" class="birthday-days">
{{ birthday.daysUntil }} Tage
{{ $t('widgets.birthdays.inDays', { count: birthday.daysUntil }) }}
</div>
</div>
</div>
<div v-else class="birthday-empty">
Keine Geburtstage von Freunden sichtbar
{{ $t('widgets.birthdays.empty') }}
</div>
</template>
@@ -37,10 +37,19 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
day: 'numeric',
month: 'short'
});

View File

@@ -21,7 +21,7 @@
</dd>
</template>
</dl>
<span v-else></span>
<span v-else>{{ $t('widgets.falukant.emptyValue') }}</span>
</template>
<script>
@@ -62,7 +62,7 @@ export default {
},
falukantDisplayName() {
const d = this.falukantData;
if (!d) return '—';
if (!d) return this.$t('widgets.falukant.emptyValue');
const titleKey = d.titleLabelTr;
const gender = d.gender;
const nameWithoutTitle = d.nameWithoutTitle ?? d.characterName;
@@ -71,11 +71,11 @@ export default {
const translatedTitle = this.$t(key);
if (translatedTitle !== key) return `${translatedTitle} ${nameWithoutTitle}`.trim();
}
return d.characterName || nameWithoutTitle || '—';
return d.characterName || nameWithoutTitle || this.$t('widgets.falukant.emptyValue');
},
falukantGenderLabel() {
const g = this.falukantData?.gender;
if (g == null || g === '') return '—';
if (g == null || g === '') return this.$t('widgets.falukant.emptyValue');
// Altersabhängige, (auf Wunsch) altertümlichere Bezeichnungen
const years = this._ageYearsFromWidgetValue(this.falukantData?.age);
@@ -93,9 +93,9 @@ export default {
},
falukantAgeLabel() {
const ageValue = this.falukantData?.age;
if (ageValue == null) return '—';
if (ageValue == null) return this.$t('widgets.falukant.emptyValue');
const years = this._ageYearsFromWidgetValue(ageValue);
if (years == null) return '—';
if (years == null) return this.$t('widgets.falukant.emptyValue');
return `${years} ${this.$t('falukant.overview.metadata.years')}`;
}
},
@@ -145,8 +145,17 @@ export default {
},
formatMoney(value) {
const n = Number(value);
if (Number.isNaN(n)) return '—';
return n.toLocaleString('de-DE');
if (Number.isNaN(n)) return this.$t('widgets.falukant.emptyValue');
return n.toLocaleString(this.getNumberLocale());
},
getNumberLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
}
}
};

View File

@@ -30,21 +30,32 @@ export default {
fallbackText() {
if (this.data == null) return '';
if (Array.isArray(this.data)) {
return this.data.length === 0 ? 'Keine Einträge' : `(${this.data.length} Einträge)`;
return this.data.length === 0
? this.$t('widgets.list.noEntries')
: this.$t('widgets.list.entriesCount', { count: this.data.length });
}
if (typeof this.data === 'object') {
const keys = Object.keys(this.data);
return keys.length === 0 ? '—' : `(${keys.length} Felder)`;
return keys.length === 0 ? '—' : this.$t('widgets.list.fieldsCount', { count: keys.length });
}
return String(this.data);
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDatum(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return String(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
weekday: 'short',
day: 'numeric',
month: 'short',

View File

@@ -7,13 +7,13 @@
rel="noopener noreferrer"
class="dashboard-widget__news-title"
>
{{ article.title || '—' }}
{{ article.title || $t('widgets.news.emptyValue') }}
</a>
<span v-else class="dashboard-widget__title-text">{{ article.title || '—' }}</span>
<span v-else class="dashboard-widget__title-text">{{ article.title || $t('widgets.news.emptyValue') }}</span>
<span v-if="article.pubDate" class="dashboard-widget__date">{{ formatNewsDate(article.pubDate) }}</span>
<p v-if="article.description" class="dashboard-widget__desc">{{ article.description }}</p>
</article>
<span v-else></span>
<span v-else>{{ $t('widgets.news.emptyValue') }}</span>
</template>
<script>
@@ -32,11 +32,20 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatNewsDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return String(dateStr);
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric',

View File

@@ -14,16 +14,16 @@
<div class="upcoming-date">
{{ formatDate(event.datum) }}
<span v-if="event.startTime && !event.allDay" class="upcoming-time">
{{ event.startTime }} Uhr
{{ $t('widgets.upcoming.timeAt', { time: event.startTime }) }}
</span>
<span v-if="event.allDay" class="upcoming-allday">Ganztägig</span>
<span v-if="event.allDay" class="upcoming-allday">{{ $t('widgets.upcoming.allDay') }}</span>
</div>
<div v-if="event.beschreibung" class="upcoming-desc">{{ event.beschreibung }}</div>
</div>
</div>
</div>
<div v-else class="upcoming-empty">
Keine anstehenden Termine
{{ $t('widgets.upcoming.empty') }}
</div>
</template>
@@ -50,6 +50,15 @@ export default {
}
},
methods: {
getDateLocale() {
const locale = this.$i18n?.locale;
return {
de: 'de-DE',
en: 'en-GB',
es: 'es-ES',
ceb: 'fil-PH'
}[locale] || 'de-DE';
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
@@ -58,13 +67,13 @@ export default {
tomorrow.setDate(tomorrow.getDate() + 1);
if (d.toDateString() === today.toDateString()) {
return 'Heute';
return this.$t('widgets.upcoming.today');
}
if (d.toDateString() === tomorrow.toDateString()) {
return 'Morgen';
return this.$t('widgets.upcoming.tomorrow');
}
return d.toLocaleDateString('de-DE', {
return d.toLocaleDateString(this.getDateLocale(), {
weekday: 'short',
day: 'numeric',
month: 'short'