From 1d5b007577da3558b67dcacbac9368672dc8a2a3 Mon Sep 17 00:00:00 2001 From: Alvin <524715@vistacollege.nl> Date: Tue, 4 Nov 2025 11:35:45 +0100 Subject: [PATCH] Implement multilingual support with Dutch and English translations across the application --- models/Item.js | 20 +- public/css/style.css | 79 ++++++++ public/index.html | 21 ++- public/js/student-reservations.js | 8 + public/js/student.js | 61 +++++-- public/js/translations.js | 293 ++++++++++++++++++++++++++++++ public/student-reservations.html | 19 +- public/student.html | 43 +++-- seed.js | 48 ++++- 9 files changed, 540 insertions(+), 52 deletions(-) create mode 100644 public/js/translations.js diff --git a/models/Item.js b/models/Item.js index 927e894..6e295da 100644 --- a/models/Item.js +++ b/models/Item.js @@ -2,12 +2,24 @@ const mongoose = require('mongoose'); const itemSchema = new mongoose.Schema({ name: { - type: String, - required: true + en: { + type: String, + required: true + }, + nl: { + type: String, + required: true + } }, description: { - type: String, - default: '' + en: { + type: String, + default: '' + }, + nl: { + type: String, + default: '' + } }, location: { type: String, diff --git a/public/css/style.css b/public/css/style.css index 7da740c..200f8e0 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -475,4 +475,83 @@ body { background-color: var(--vista-coral); border-color: var(--vista-coral); color: var(--vista-white); +} + +/* Language Toggle Styles */ +.language-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: 1rem; +} + +.language-switch { + position: relative; + display: inline-block; + width: 60px; + height: 28px; +} + +.language-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.language-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--vista-coral); + transition: .4s; + border-radius: 34px; +} + +.language-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .language-slider { + background-color: var(--vista-blue); +} + +input:checked + .language-slider:before { + transform: translateX(32px); +} + +.language-label { + color: var(--vista-white); + font-weight: 500; + min-width: 20px; + text-align: center; +} + +.language-label.active { + font-weight: bold; +} + +/* Language toggle for login page */ +.language-toggle-container { + display: flex; + justify-content: center; + align-items: center; +} + +.language-toggle-container .language-toggle { + margin-right: 0; +} + +.language-toggle-container .language-label { + color: var(--vista-blue) !important; } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 9a4a444..655f923 100644 --- a/public/index.html +++ b/public/index.html @@ -12,23 +12,33 @@
+
+
+ NL + + EN +
+
-

Inloggen

+

Inloggen

- +
- +
- -

Nieuwe student? Registreer hier

+ +

Geen account? Registreer hier

@@ -37,6 +47,7 @@
+ \ No newline at end of file diff --git a/public/js/student-reservations.js b/public/js/student-reservations.js index cd9e237..0b9dedc 100644 --- a/public/js/student-reservations.js +++ b/public/js/student-reservations.js @@ -20,6 +20,14 @@ async function initializePage() { startAutoRefresh(); } +// Reload content when language changes +function reloadContent() { + displayReservations(); +} + +// Make reloadContent available globally for translation manager +window.reloadContent = reloadContent; + // Display user info function displayUserInfo() { const username = localStorage.getItem('username'); diff --git a/public/js/student.js b/public/js/student.js index 80f9d97..7bae553 100644 --- a/public/js/student.js +++ b/public/js/student.js @@ -21,6 +21,15 @@ async function initializePage() { startAutoRefresh(); } +// Reload content when language changes +function reloadContent() { + displayItems(); + loadMyReservations(); +} + +// Make reloadContent available globally for translation manager +window.reloadContent = reloadContent; + // Display user info function displayUserInfo() { const username = localStorage.getItem('username'); @@ -112,12 +121,20 @@ function displayItems() { filteredItems = filteredItems.filter(item => item.location === locationFilter); } - // Apply search filter + // Apply search filter with translation support if (searchTerm.trim() !== '') { - filteredItems = filteredItems.filter(item => - item.name.toLowerCase().includes(searchTerm) || - (item.description && item.description.toLowerCase().includes(searchTerm)) - ); + filteredItems = filteredItems.filter(item => { + if (window.translationManager) { + const localizedItem = window.translationManager.getLocalizedItem(item); + return localizedItem.name.toLowerCase().includes(searchTerm) || + (localizedItem.description && localizedItem.description.toLowerCase().includes(searchTerm)); + } else { + // Fallback for when translation manager isn't loaded yet (default to Dutch) + const name = item.name?.nl || item.name?.en || item.name || ''; + const description = item.description?.nl || item.description?.en || item.description || ''; + return name.toLowerCase().includes(searchTerm) || description.toLowerCase().includes(searchTerm); + } + }); } // Update view mode buttons active state @@ -133,13 +150,19 @@ function displayItems() { if (currentViewMode === 'grid') { // Display items in grid view const gridContainer = document.getElementById('itemsGrid'); - gridContainer.innerHTML = filteredItems.map(item => ` + gridContainer.innerHTML = filteredItems.map(item => { + const localizedItem = window.translationManager ? window.translationManager.getLocalizedItem(item) : { + name: item.name?.nl || item.name?.en || item.name || 'Onbekend Artikel', + description: item.description?.nl || item.description?.en || item.description || '' + }; + + return `
- ${item.name} + ${localizedItem.name}
-
${item.name}
-

${item.description || 'No description available'}

+
${localizedItem.name}
+

${localizedItem.description || 'No description available'}

Location: ${item.location}
Available: ${item.quantity - (item.reserved || 0)} @@ -151,15 +174,22 @@ function displayItems() {

- `).join(''); + `; + }).join(''); } else { // Display items in list view const itemsListBody = document.getElementById('itemsListBody'); - itemsListBody.innerHTML = filteredItems.map(item => ` + itemsListBody.innerHTML = filteredItems.map(item => { + const localizedItem = window.translationManager ? window.translationManager.getLocalizedItem(item) : { + name: item.name?.nl || item.name?.en || item.name || 'Onbekend Artikel', + description: item.description?.nl || item.description?.en || item.description || '' + }; + + return ` - ${item.name} - ${item.name} - ${item.description || 'No description available'} + ${localizedItem.name} + ${localizedItem.name} + ${localizedItem.description || 'No description available'} ${item.location} ${item.quantity - (item.reserved || 0)} @@ -169,7 +199,8 @@ function displayItems() { } - `).join(''); + `; + }).join(''); } } diff --git a/public/js/translations.js b/public/js/translations.js new file mode 100644 index 0000000..4a7caf2 --- /dev/null +++ b/public/js/translations.js @@ -0,0 +1,293 @@ +// Translation utility for the warehouse management system + +const translations = { + en: { + // Navigation + 'school-warehouse': 'School Warehouse', + 'available-items': 'Available Items', + 'my-reservations': 'My Reservations', + 'logout': 'Logout', + 'inventory': 'Inventory', + 'add-new-item': 'Add New Item', + 'reservations': 'Reservations', + + // Page titles + 'warehouse-dashboard-admin': 'Warehouse Dashboard - Admin', + 'warehouse-dashboard-student': 'Warehouse Dashboard - Student', + 'my-reservations-student': 'My Reservations - Student', + 'login': 'Login', + 'register': 'Register', + + // Forms + 'username': 'Username', + 'password': 'Password', + 'remember-me': 'Remember Me', + 'no-account': "Don't have an account?", + 'student-registration': 'Student Registration', + 'confirm-password': 'Confirm Password', + 'already-account': 'Already have an account?', + + // Search and filters + 'search-items': 'Search items...', + 'search-reservations': 'Search reservations...', + 'clear-search': 'Clear search', + 'all-locations': 'All Locations', + 'all-status': 'All Status', + 'filter-reservations': 'Filter Reservations', + 'location': 'Location', + 'status': 'Status', + 'search': 'Search', + + // Table headers + 'image': 'Image', + 'item-name': 'Item Name', + 'description': 'Description', + 'quantity': 'Quantity', + 'quantity-available': 'Quantity Available', + 'action': 'Action', + 'actions': 'Actions', + 'reserved-date': 'Reserved Date', + + // View modes + 'grid': 'Grid', + 'list': 'List', + + // Status values + 'pending': 'Pending', + 'approved': 'Approved', + 'rejected': 'Rejected', + 'return-pending': 'Return Pending', + 'returned': 'Returned', + + // Buttons and actions + 'reserve': 'Reserve', + 'cancel': 'Cancel', + 'return': 'Return', + 'approve': 'Approve', + 'reject': 'Reject', + 'edit': 'Edit', + 'delete': 'Delete', + + // Messages + 'loading': 'Loading...', + 'loading-items': 'Loading items...', + 'failed-load-items': 'Failed to load items', + 'failed-load-reservations': 'Failed to load reservations', + 'failed-reserve-item': 'Failed to reserve item', + 'failed-cancel-reservation': 'Failed to cancel reservation', + 'failed-request-return': 'Failed to request return', + 'confirm-cancel-reservation': 'Are you sure you want to cancel this reservation?', + 'confirm-return-request': 'Are you sure you want to request return for this item? An admin will need to approve the return.', + 'return-request-success': 'Return requested successfully! An admin will review your request.', + + // Management + 'inventory-management': 'Inventory Management', + 'reservation-management': 'Reservation Management' + }, + nl: { + // Navigation + 'school-warehouse': 'School Magazijn', + 'available-items': 'Beschikbare Artikelen', + 'my-reservations': 'Mijn Reserveringen', + 'logout': 'Uitloggen', + 'inventory': 'Voorraad', + 'add-new-item': 'Nieuw Artikel Toevoegen', + 'reservations': 'Reserveringen', + + // Page titles + 'warehouse-dashboard-admin': 'Magazijn Dashboard - Admin', + 'warehouse-dashboard-student': 'Magazijn Dashboard - Student', + 'my-reservations-student': 'Mijn Reserveringen - Student', + 'login': 'Inloggen', + 'register': 'Registreren', + + // Forms + 'username': 'Gebruikersnaam', + 'password': 'Wachtwoord', + 'remember-me': 'Onthoud mij', + 'no-account': 'Geen account?', + 'student-registration': 'Student Registratie', + 'confirm-password': 'Bevestig Wachtwoord', + 'already-account': 'Al een account?', + + // Search and filters + 'search-items': 'Zoek artikelen...', + 'search-reservations': 'Zoek reserveringen...', + 'clear-search': 'Zoekopdracht wissen', + 'all-locations': 'Alle Locaties', + 'all-status': 'Alle Statussen', + 'filter-reservations': 'Filter Reserveringen', + 'location': 'Locatie', + 'status': 'Status', + 'search': 'Zoeken', + + // Table headers + 'image': 'Afbeelding', + 'item-name': 'Artikelnaam', + 'description': 'Beschrijving', + 'quantity': 'Hoeveelheid', + 'quantity-available': 'Beschikbare Hoeveelheid', + 'action': 'Actie', + 'actions': 'Acties', + 'reserved-date': 'Reserveringsdatum', + + // View modes + 'grid': 'Raster', + 'list': 'Lijst', + + // Status values + 'pending': 'In Behandeling', + 'approved': 'Goedgekeurd', + 'rejected': 'Afgewezen', + 'return-pending': 'Retour in Behandeling', + 'returned': 'Geretourneerd', + + // Buttons and actions + 'reserve': 'Reserveren', + 'cancel': 'Annuleren', + 'return': 'Retourneren', + 'approve': 'Goedkeuren', + 'reject': 'Afwijzen', + 'edit': 'Bewerken', + 'delete': 'Verwijderen', + + // Messages + 'loading': 'Laden...', + 'loading-items': 'Artikelen laden...', + 'failed-load-items': 'Kon artikelen niet laden', + 'failed-load-reservations': 'Kon reserveringen niet laden', + 'failed-reserve-item': 'Kon artikel niet reserveren', + 'failed-cancel-reservation': 'Kon reservering niet annuleren', + 'failed-request-return': 'Kon retour niet aanvragen', + 'confirm-cancel-reservation': 'Weet je zeker dat je deze reservering wilt annuleren?', + 'confirm-return-request': 'Weet je zeker dat je retour wilt aanvragen voor dit artikel? Een admin moet de retour goedkeuren.', + 'return-request-success': 'Retour succesvol aangevraagd! Een admin zal je verzoek beoordelen.', + + // Management + 'inventory-management': 'Voorraadbeheer', + 'reservation-management': 'Reserveringsbeheer' + } +}; + +// Translation manager class +class TranslationManager { + constructor() { + this.currentLanguage = localStorage.getItem('language') || 'nl'; // Default to Dutch + this.init(); + } + + init() { + // Apply saved language preference + this.applyTranslations(); + + // Set up language toggle if it exists + const languageToggle = document.getElementById('languageToggle'); + if (languageToggle) { + languageToggle.addEventListener('change', (e) => { + this.setLanguage(e.target.checked ? 'en' : 'nl'); + }); + + // Set toggle state based on current language (Dutch is default/unchecked) + languageToggle.checked = this.currentLanguage === 'en'; + this.updateLanguageLabels(); + } + } + + updateLanguageLabels() { + const nlLabel = document.getElementById('nlLabel'); + const enLabel = document.getElementById('enLabel'); + + if (nlLabel && enLabel) { + nlLabel.classList.toggle('active', this.currentLanguage === 'nl'); + enLabel.classList.toggle('active', this.currentLanguage === 'en'); + } + } + + setLanguage(lang) { + if (lang !== 'en' && lang !== 'nl') { + console.warn('Unsupported language:', lang); + return; + } + + this.currentLanguage = lang; + localStorage.setItem('language', lang); + this.applyTranslations(); + this.updateLanguageLabels(); + + // Reload dynamic content if function exists + if (window.reloadContent) { + window.reloadContent(); + } + } + + getLanguage() { + return this.currentLanguage; + } + + translate(key) { + const translation = translations[this.currentLanguage]?.[key]; + if (!translation) { + console.warn(`Translation missing for key: ${key} in language: ${this.currentLanguage}`); + // Fallback to Dutch first, then English, then key itself + return translations['nl']?.[key] || translations['en']?.[key] || key; + } + return translation; + } + + applyTranslations() { + // Update all elements with data-translate attribute + document.querySelectorAll('[data-translate]').forEach(element => { + const key = element.getAttribute('data-translate'); + const translation = this.translate(key); + + if (element.tagName === 'INPUT' && element.type === 'submit') { + element.value = translation; + } else if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) { + element.placeholder = translation; + } else if (element.hasAttribute('title')) { + element.title = translation; + } else { + element.textContent = translation; + } + }); + + // Update page title if it has data-translate + const titleElement = document.querySelector('title[data-translate]'); + if (titleElement) { + const key = titleElement.getAttribute('data-translate'); + document.title = this.translate(key); + } + } + + // Get translated item data based on current language + getLocalizedItem(item) { + return { + ...item, + name: item.name?.[this.currentLanguage] || item.name?.nl || item.name?.en || item.name || 'Unknown Item', + description: item.description?.[this.currentLanguage] || item.description?.nl || item.description?.en || item.description || '' + }; + } + + // Get translated status text + getStatusText(status) { + const statusMap = { + 'PENDING': 'pending', + 'APPROVED': 'approved', + 'REJECTED': 'rejected', + 'RETURN_PENDING': 'return-pending', + 'RETURNED': 'returned' + }; + + return this.translate(statusMap[status] || status.toLowerCase()); + } +} + +// Initialize translation manager when DOM is loaded +let translationManager; +document.addEventListener('DOMContentLoaded', () => { + translationManager = new TranslationManager(); +}); + +// Export for use in other files +window.TranslationManager = TranslationManager; +window.translationManager = translationManager; \ No newline at end of file diff --git a/public/student-reservations.html b/public/student-reservations.html index b26ae22..7be1492 100644 --- a/public/student-reservations.html +++ b/public/student-reservations.html @@ -12,29 +12,37 @@
-

Mijn Reserveringen

+

Mijn Reserveringen

@@ -97,6 +105,7 @@
+ \ No newline at end of file diff --git a/public/student.html b/public/student.html index 1c2e0ce..e5b5ad8 100644 --- a/public/student.html +++ b/public/student.html @@ -3,7 +3,7 @@ - Magazijn Dashboard - Student + Magazijn Dashboard - Student @@ -12,22 +12,30 @@