feat: Add language selector and Dutch translations
This commit is contained in:
parent
fb951acb72
commit
f20f3c960d
@ -2,7 +2,7 @@
|
||||
export const API_VERSION = 'v1'
|
||||
|
||||
// API Base URL
|
||||
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
|
||||
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
// API Endpoints
|
||||
export const API_ENDPOINTS = {
|
||||
@ -33,7 +33,6 @@ export const API_ENDPOINTS = {
|
||||
BASE: '/lists',
|
||||
BY_ID: (id: string) => `/lists/${id}`,
|
||||
STATUS: (id: string) => `/lists/${id}/status`,
|
||||
STATUSES: '/lists/statuses',
|
||||
ITEMS: (listId: string) => `/lists/${listId}/items`,
|
||||
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
|
||||
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
|
||||
@ -62,13 +61,15 @@ export const API_ENDPOINTS = {
|
||||
SETTINGS: (groupId: string) => `/groups/${groupId}/settings`,
|
||||
ROLES: (groupId: string) => `/groups/${groupId}/roles`,
|
||||
ROLE: (groupId: string, roleId: string) => `/groups/${groupId}/roles/${roleId}`,
|
||||
GENERATE_SCHEDULE: (groupId: string) => `/groups/${groupId}/chores/generate-schedule`,
|
||||
CHORE_HISTORY: (groupId: string) => `/groups/${groupId}/chores/history`,
|
||||
},
|
||||
|
||||
// Invites
|
||||
INVITES: {
|
||||
BASE: '/invites',
|
||||
BY_ID: (id: string) => `/invites/${id}`,
|
||||
ACCEPT: '/invites/accept',
|
||||
ACCEPT: (id: string) => `/invites/accept/${id}`,
|
||||
DECLINE: (id: string) => `/invites/decline/${id}`,
|
||||
REVOKE: (id: string) => `/invites/revoke/${id}`,
|
||||
LIST: '/invites',
|
||||
@ -120,4 +121,12 @@ export const API_ENDPOINTS = {
|
||||
METRICS: '/health/metrics',
|
||||
LOGS: '/health/logs',
|
||||
},
|
||||
|
||||
CHORES: {
|
||||
BASE: '/chores',
|
||||
BY_ID: (id: number) => `/chores/${id}`,
|
||||
HISTORY: (id: number) => `/chores/${id}/history`,
|
||||
ASSIGNMENTS: (choreId: number) => `/chores/${choreId}/assignments`,
|
||||
ASSIGNMENT_BY_ID: (id: number) => `/chores/assignments/${id}`,
|
||||
},
|
||||
}
|
||||
|
@ -627,5 +627,15 @@
|
||||
"sampleTodosHeader": "Beispiel-Todos (aus IndexPage-Daten)",
|
||||
"totalCountLabel": "Gesamtzahl aus Meta:",
|
||||
"noTodos": "Keine Todos zum Anzeigen."
|
||||
},
|
||||
"languageSelector": {
|
||||
"title": "Sprache",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"nl": "Nederlands",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
}
|
||||
}
|
||||
}
|
@ -555,5 +555,15 @@
|
||||
"sampleTodosHeader": "Sample Todos (from IndexPage data)",
|
||||
"totalCountLabel": "Total count from meta:",
|
||||
"noTodos": "No todos to display."
|
||||
},
|
||||
"languageSelector": {
|
||||
"title": "Language",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"nl": "Nederlands",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
}
|
||||
}
|
||||
}
|
@ -627,5 +627,15 @@
|
||||
"sampleTodosHeader": "Tareas de ejemplo (de datos de IndexPage)",
|
||||
"totalCountLabel": "Recuento total de meta:",
|
||||
"noTodos": "No hay tareas para mostrar."
|
||||
},
|
||||
"languageSelector": {
|
||||
"title": "Idioma",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"nl": "Nederlands",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
}
|
||||
}
|
||||
}
|
@ -627,5 +627,15 @@
|
||||
"sampleTodosHeader": "Exemples de tâches (depuis les données IndexPage)",
|
||||
"totalCountLabel": "Nombre total depuis meta :",
|
||||
"noTodos": "Aucune tâche à afficher."
|
||||
},
|
||||
"languageSelector": {
|
||||
"title": "Langue",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"nl": "Nederlands",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
}
|
||||
}
|
||||
}
|
@ -627,5 +627,15 @@
|
||||
"sampleTodosHeader": "Voorbeeldtaken (uit IndexPage-gegevens)",
|
||||
"totalCountLabel": "Totaal aantal uit meta:",
|
||||
"noTodos": "Geen taken om weer te geven."
|
||||
},
|
||||
"languageSelector": {
|
||||
"title": "Taal",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"nl": "Nederlands",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,28 @@
|
||||
<div class="main-layout">
|
||||
<header class="app-header">
|
||||
<div class="toolbar-title">mitlist</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex align-end">
|
||||
<div class="language-selector" v-if="authStore.isAuthenticated">
|
||||
<button @click="toggleLanguageMenu" class="language-menu-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="20px" fill="#ff7b54">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="m12.87 15.07-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z" />
|
||||
</svg>
|
||||
<span class="current-language">{{ currentLanguageCode.toUpperCase() }}</span>
|
||||
</button>
|
||||
<div v-if="languageMenuOpen" class="dropdown-menu language-dropdown" ref="languageMenuDropdown">
|
||||
<div class="dropdown-header">{{ $t('languageSelector.title') }}</div>
|
||||
<a v-for="(name, code) in availableLanguages" :key="code" href="#" @click.prevent="changeLanguage(code)"
|
||||
class="language-option" :class="{ 'active': currentLanguageCode === code }">
|
||||
{{ name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-menu" v-if="authStore.isAuthenticated">
|
||||
<button @click="toggleUserMenu" class="user-menu-button">
|
||||
<!-- Placeholder for user icon -->
|
||||
@ -15,6 +37,7 @@
|
||||
<a href="#" @click.prevent="handleLogout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="page-container">
|
||||
@ -53,13 +76,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineComponent, onMounted } from 'vue';
|
||||
import { ref, defineComponent, onMounted, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import OfflineIndicator from '@/components/OfflineIndicator.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
import { useGroupStore } from '@/stores/groupStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineComponent({
|
||||
name: 'MainLayout'
|
||||
@ -70,6 +94,7 @@ const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const groupStore = useGroupStore();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
// Add initialization logic
|
||||
const initializeApp = async () => {
|
||||
@ -90,6 +115,12 @@ onMounted(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
groupStore.fetchGroups();
|
||||
}
|
||||
|
||||
// Load saved language from localStorage
|
||||
const savedLanguage = localStorage.getItem('language');
|
||||
if (savedLanguage && ['en', 'de', 'nl', 'fr', 'es'].includes(savedLanguage)) {
|
||||
locale.value = savedLanguage;
|
||||
}
|
||||
});
|
||||
|
||||
const userMenuOpen = ref(false);
|
||||
@ -103,6 +134,37 @@ onClickOutside(userMenuDropdown, () => {
|
||||
userMenuOpen.value = false;
|
||||
}, { ignore: ['.user-menu-button'] });
|
||||
|
||||
// Language selector state and functions
|
||||
const languageMenuOpen = ref(false);
|
||||
const languageMenuDropdown = ref<HTMLElement | null>(null);
|
||||
|
||||
const availableLanguages = computed(() => ({
|
||||
en: t('languageSelector.languages.en'),
|
||||
de: t('languageSelector.languages.de'),
|
||||
nl: t('languageSelector.languages.nl'),
|
||||
fr: t('languageSelector.languages.fr'),
|
||||
es: t('languageSelector.languages.es')
|
||||
}));
|
||||
|
||||
const currentLanguageCode = computed(() => locale.value);
|
||||
|
||||
const toggleLanguageMenu = () => {
|
||||
languageMenuOpen.value = !languageMenuOpen.value;
|
||||
};
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
locale.value = languageCode;
|
||||
localStorage.setItem('language', languageCode);
|
||||
languageMenuOpen.value = false;
|
||||
notificationStore.addNotification({
|
||||
type: 'success',
|
||||
message: `Language changed to ${availableLanguages.value[languageCode as keyof typeof availableLanguages.value]}`,
|
||||
});
|
||||
};
|
||||
|
||||
onClickOutside(languageMenuDropdown, () => {
|
||||
languageMenuOpen.value = false;
|
||||
}, { ignore: ['.language-menu-button'] });
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
@ -163,23 +225,61 @@ const navigateToGroups = () => {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
.language-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-button {
|
||||
.language-menu-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 123, 84, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.current-language {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
min-width: 180px;
|
||||
|
||||
.dropdown-header {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(255, 123, 84, 0.1);
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +307,25 @@ const navigateToGroups = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.page-container {
|
||||
flex-grow: 1;
|
||||
|
@ -40,6 +40,7 @@ const i18n = createI18n({
|
||||
de: deMessages,
|
||||
fr: frMessages,
|
||||
es: esMessages,
|
||||
nl: nlMessages,
|
||||
},
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user