diff --git a/backend/services/authService.js b/backend/services/authService.js
index 024f349..78af60a 100644
--- a/backend/services/authService.js
+++ b/backend/services/authService.js
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import User from '../models/community/user.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
+import UserParamValue from '../models/type/user_param_value.js';
import { sendAccountActivationEmail, sendPasswordResetEmail } from './emailService.js';
import { sequelize } from '../utils/sequelize.js';
import { Op } from 'sequelize';
@@ -143,6 +144,16 @@ export const loginUser = async ({ username, password }) => {
const mappedParams = params.map(param => {
return { 'name': param.paramType.description, 'value': param.value };
});
+ const uiLocaleCodes = ['de', 'en', 'ceb', 'es'];
+ const langEntry = mappedParams.find((p) => p.name === 'language');
+ if (langEntry?.value && !uiLocaleCodes.includes(langEntry.value)) {
+ const idNum = parseInt(langEntry.value, 10);
+ const lookupId = Number.isNaN(idNum) ? langEntry.value : idNum;
+ const upv = await UserParamValue.findOne({ where: { id: lookupId } });
+ if (upv?.value && uiLocaleCodes.includes(upv.value)) {
+ langEntry.value = upv.value;
+ }
+ }
console.log('return user');
return {
id: user.hashedId,
diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js
index 5ca2db2..a4e15c1 100644
--- a/backend/utils/initializeTypes.js
+++ b/backend/utils/initializeTypes.js
@@ -67,7 +67,7 @@ const initializeTypes = async () => {
}
const valuesList = {
gender: ['male', 'female', 'transfemale', 'transmale', 'nonbinary'],
- language: ['de', 'en'],
+ language: ['de', 'en', 'ceb', 'es'],
eyecolor: ['blue', 'green', 'brown', 'black', 'grey', 'hazel', 'amber', 'red', 'other'],
haircolor: ['black', 'brown', 'blonde', 'red', 'grey', 'white', 'other'],
hairlength: ['short', 'medium', 'long', 'bald', 'other'],
diff --git a/backend/utils/initializeWidgetTypes.js b/backend/utils/initializeWidgetTypes.js
index 5a4720b..72aa8e3 100644
--- a/backend/utils/initializeWidgetTypes.js
+++ b/backend/utils/initializeWidgetTypes.js
@@ -1,5 +1,9 @@
import WidgetType from '../models/type/widget_type.js';
+/**
+ * Default rows for type.widget_type. Labels/descriptions are German for DB/admin readability;
+ * the web app maps known `endpoint` values to localized strings (see LoggedInView / home.dashboard.widgetLabels).
+ */
const DEFAULT_WIDGET_TYPES = [
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue
index 612932e..8a3f3e4 100644
--- a/frontend/src/components/AppFooter.vue
+++ b/frontend/src/components/AppFooter.vue
@@ -4,10 +4,12 @@
@@ -23,7 +25,7 @@
{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
dialog.dialog.localTitle }}
- System bereit
+ {{ $t('appShell.footer.systemReady') }}
@@ -72,7 +74,7 @@ export default {
},
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
async showFalukantDaemonStatus() {
- showInfo(this, 'Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar.');
+ showInfo(this, this.$t('appShell.footer.systemStatusUnavailable'));
},
handleDaemonMessage() {
// Status-Events werden hier bewusst nicht verarbeitet.
diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue
index 8a0d789..835bfb1 100644
--- a/frontend/src/components/AppHeader.vue
+++ b/frontend/src/components/AppHeader.vue
@@ -5,21 +5,34 @@
YourPart
- Community-Plattform
+ {{ $t('appShell.header.tagline') }}
@@ -29,11 +42,22 @@
@@ -144,6 +201,38 @@ export default {
color: #8a5411;
}
+.header-lang {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin: 0;
+ font-size: 0.72rem;
+ color: rgba(95, 75, 57, 0.85);
+}
+
+.header-lang__label {
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.header-lang__select {
+ min-width: 7.5rem;
+ max-width: 11rem;
+ padding: 4px 8px;
+ border-radius: 8px;
+ border: 1px solid rgba(93, 64, 55, 0.18);
+ background: rgba(255, 255, 255, 0.85);
+ color: #3a2a1b;
+ font-size: 0.72rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.header-lang__select:focus {
+ outline: 2px solid rgba(248, 162, 43, 0.45);
+ outline-offset: 1px;
+}
+
.connection-status {
display: flex;
align-items: center;
diff --git a/frontend/src/components/DashboardWidget.vue b/frontend/src/components/DashboardWidget.vue
index b3e6433..397b1e8 100644
--- a/frontend/src/components/DashboardWidget.vue
+++ b/frontend/src/components/DashboardWidget.vue
@@ -5,12 +5,12 @@
:data-widget-id="widgetId"
>
diff --git a/frontend/src/components/SettingsWidget.vue b/frontend/src/components/SettingsWidget.vue
index b9fb7bc..4eed3c8 100644
--- a/frontend/src/components/SettingsWidget.vue
+++ b/frontend/src/components/SettingsWidget.vue
@@ -103,10 +103,6 @@ export default {
type: String,
required: true
}
- },
- data: {
- settings: [],
- possibleVisibilities: [],
},
computed: {
...mapGetters(['user']),
@@ -170,6 +166,14 @@ export default {
settingId: settingId,
value: value
});
+ if (setting?.name === 'language' && Array.isArray(setting.options)) {
+ const opt = setting.options.find((o) => String(o.id) === String(value));
+ const code = opt?.value;
+ const supported = ['de', 'en', 'ceb', 'es'];
+ if (code && supported.includes(code)) {
+ this.$store.dispatch('setLanguage', code);
+ }
+ }
this.fetchSettings();
} catch (err) {
console.error('Error updating setting:', err);
@@ -238,6 +242,7 @@ export default {
data() {
return {
settings: [],
+ possibleVisibilities: [],
userEmail: "",
userUsername: "",
};
diff --git a/frontend/src/components/TermineWidget.vue b/frontend/src/components/TermineWidget.vue
index e218eff..7eea212 100644
--- a/frontend/src/components/TermineWidget.vue
+++ b/frontend/src/components/TermineWidget.vue
@@ -1,10 +1,10 @@