Implement multilingual support with Dutch and English translations across the application

This commit is contained in:
Alvin
2025-11-04 11:35:45 +01:00
parent 278e9353f9
commit 1d5b007577
9 changed files with 540 additions and 52 deletions

View File

@@ -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');

View File

@@ -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 `
<div class="col-md-4 col-lg-3 mb-4">
<div class="card h-100" style="cursor: pointer" onclick='showItemDetails(${JSON.stringify(item).replace(/"/g, '&quot;')})'>
<img src="${item.imageUrl || '/images/default-item.png'}" class="card-img-top" alt="${item.name}" style="height: 200px; object-fit: cover;">
<img src="${item.imageUrl || '/images/default-item.png'}" class="card-img-top" alt="${localizedItem.name}" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title">${item.name}</h5>
<p class="card-text small text-muted">${item.description || 'No description available'}</p>
<h5 class="card-title">${localizedItem.name}</h5>
<p class="card-text small text-muted">${localizedItem.description || 'No description available'}</p>
<p class="card-text">
<small class="text-muted">Location: ${item.location}</small><br>
<small class="text-muted">Available: ${item.quantity - (item.reserved || 0)}</small>
@@ -151,15 +174,22 @@ function displayItems() {
</div>
</div>
</div>
`).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 `
<tr class="item-row" style="cursor: pointer" onclick="showItemDetails(${JSON.stringify(item).replace(/"/g, '&quot;')})">
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${item.name}"></td>
<td>${item.name}</td>
<td>${item.description || 'No description available'}</td>
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${localizedItem.name}"></td>
<td>${localizedItem.name}</td>
<td>${localizedItem.description || 'No description available'}</td>
<td>${item.location}</td>
<td>${item.quantity - (item.reserved || 0)}</td>
<td>
@@ -169,7 +199,8 @@ function displayItems() {
}
</td>
</tr>
`).join('');
`;
}).join('');
}
}

293
public/js/translations.js Normal file
View File

@@ -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;