## Complete Multi-language System & Navigation Enhancement

### Features Added:
- **Multi-language Support**: Dutch/English with URL-based switching (?lang=nl|en)
- **Theme Customization**: Configurable header/navigation colors via config.json
- **Navigation Improvements**: Active states, dropdown chevron icons, visual distinction
- **Mobile Responsive**: Separate desktop/mobile search layouts
- **Template System**: Fixed rendering pipeline for all partials

### Technical Fixes:
- Fixed language file path (engine/lang/ vs engine/core/class/../lang/)
- Added template data rendering to layout template
- Implemented navigation active state for default/home page
- Added chevron icons to dropdown folders for visual distinction
- Removed hardcoded navigation opacity class for theme colors

### Files Modified:
- config.json: Added theme and language configuration
- engine/core/class/CodePressCMS.php: Multi-language and navigation logic
- engine/templates/: Enhanced header, footer, navigation, layout
- engine/lang/: Dutch and English translation files
- public/assets/css/mobile.css: Mobile responsive fixes

### Result:
Fully functional multi-language CMS with proper navigation states and theme customization.
This commit is contained in:
2025-11-22 15:29:47 +01:00
parent 863661612a
commit a2b7fcb1a8
9 changed files with 431 additions and 121 deletions

View File

@@ -24,6 +24,8 @@ class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
private $currentLanguage;
private $translations = [];
/**
* Constructor - Initialize the CMS with configuration
@@ -32,6 +34,8 @@ class CodePressCMS {
*/
public function __construct($config) {
$this->config = $config;
$this->currentLanguage = $this->getCurrentLanguage();
$this->translations = $this->loadTranslations($this->currentLanguage);
$this->buildMenu();
if (isset($_GET['search'])) {
@@ -39,6 +43,46 @@ class CodePressCMS {
}
}
/**
* Get current language from URL or use default
*
* @return string Current language code
*/
private function getCurrentLanguage() {
return $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl';
}
/**
* Load translations for specified language
*
* @param string $lang Language code
* @return array Translations array
*/
private function loadTranslations($lang) {
$langFile = __DIR__ . '/../../lang/' . $lang . '.php';
error_log("Loading language file: " . $langFile);
if (file_exists($langFile)) {
$translations = include $langFile;
error_log("Loaded translations for " . $lang . ": " . print_r($translations, true));
return $translations;
}
// Fallback to default language
$defaultLang = $this->config['language']['default'] ?? 'nl';
$defaultLangFile = __DIR__ . '/../../lang/' . $defaultLang . '.php';
error_log("Fallback to default language: " . $defaultLangFile);
return file_exists($defaultLangFile) ? include $defaultLangFile : [];
}
/**
* Get translated text
*
* @param string $key Translation key
* @return string Translated text
*/
public function t($key) {
return $this->translations[$key] ?? $key;
}
/**
* Build menu structure from content directory
*
@@ -263,12 +307,12 @@ class CodePressCMS {
*/
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
$content = '<h2>' . $this->t('search') . ' ' . $this->t('results_found') . ': "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
$content .= '<p>' . $this->t('no_results') . '.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
$content .= '<p>' . count($this->searchResults) . ' ' . $this->t('results_found') . ':</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
@@ -593,7 +637,7 @@ class CodePressCMS {
$result = $this->parseMarkdown($content);
// Set special title for guide
$result['title'] = 'Handleiding - CodePress CMS';
$result['title'] = $this->t('manual') . ' - CodePress CMS';
return $result;
}
@@ -647,11 +691,8 @@ class CodePressCMS {
sort($items);
$hasContent = false;
$content .= '<div class="row">';
// Subdirectories
$subdirs = [];
$files = [];
// Collect all items
$allItems = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
@@ -660,57 +701,46 @@ class CodePressCMS {
$relativePath = $pagePath ? $pagePath . '/' . $item : $item;
if (is_dir($itemPath)) {
$subdirs[] = [
$allItems[] = [
'name' => ucfirst($item),
'path' => $relativePath,
'url' => '?page=' . $relativePath
'url' => '?page=' . $relativePath,
'icon' => 'bi-folder',
'type' => 'directory'
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$extractedTitle = $this->extractPageTitle($itemPath);
$fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME));
$pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath);
$files[] = [
$icon = pathinfo($item, PATHINFO_EXTENSION) === 'md' ? 'bi-file-text' :
(pathinfo($item, PATHINFO_EXTENSION) === 'php' ? 'bi-file-code' : 'bi-file-earmark');
$allItems[] = [
'name' => $fileTitle,
'path' => $pathWithoutExt,
'url' => '?page=' . $pathWithoutExt,
'type' => pathinfo($item, PATHINFO_EXTENSION)
'icon' => $icon,
'type' => 'file'
];
}
}
// Display subdirectories
if (!empty($subdirs)) {
$content .= '<div class="col-md-6">';
$content .= '<h3>📁 Mappen</h3>';
// Display all items in a single column
if (!empty($allItems)) {
$content .= '<div class="list-group">';
foreach ($subdirs as $subdir) {
$content .= '<a href="' . htmlspecialchars($subdir['url']) . '" class="list-group-item list-group-item-action">';
$content .= '<i class="bi bi-folder"></i> ' . htmlspecialchars($subdir['name']);
foreach ($allItems as $item) {
$content .= '<a href="' . htmlspecialchars($item['url']) . '" class="list-group-item list-group-item-action d-flex align-items-center">';
$content .= '<i class="bi ' . $item['icon'] . ' me-3"></i>';
$content .= '<span>' . htmlspecialchars($item['name']) . '</span>';
$content .= '</a>';
}
$content .= '</div></div>';
$hasContent = true;
}
// Display files
if (!empty($files)) {
$content .= '<div class="col-md-6">';
$content .= '<h3>📄 Pagina\'s</h3>';
$content .= '<div class="list-group">';
foreach ($files as $file) {
$icon = $file['type'] === 'md' ? 'bi-file-text' : ($file['type'] === 'php' ? 'bi-file-code' : 'bi-file-earmark');
$content .= '<a href="' . htmlspecialchars($file['url']) . '" class="list-group-item list-group-item-action">';
$content .= '<i class="bi ' . $icon . '"></i> ' . htmlspecialchars($file['name']);
$content .= '</a>';
}
$content .= '</div></div>';
$content .= '</div>';
$hasContent = true;
}
$content .= '</div>';
if (!$hasContent) {
$content .= '<p>Deze map is leeg.</p>';
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
}
return [
@@ -726,8 +756,8 @@ class CodePressCMS {
*/
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
'title' => $this->t('page_not_found'),
'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>'
];
}
@@ -748,7 +778,7 @@ class CodePressCMS {
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$breadcrumb = $this->generateBreadcrumb();
// Get homepage title
$homepageTitle = $this->getHomepageTitle();
@@ -764,30 +794,57 @@ class CodePressCMS {
'default_page' => $this->config['default_page'],
'homepage' => $this->config['default_page'],
'homepage_title' => $homepageTitle,
'is_homepage' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']),
'home_active_class' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']) ? 'active' : '',
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
'author_website' => $this->config['author']['website'] ?? '#',
'author_git' => $this->config['author']['git'] ?? '#',
'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system',
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based'
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based',
// Theme colors
'header_color' => $this->config['theme']['header_color'] ?? '#0d6efd',
'header_font_color' => $this->config['theme']['header_font_color'] ?? '#ffffff',
'navigation_color' => $this->config['theme']['navigation_color'] ?? '#f8f9fa',
'navigation_font_color' => $this->config['theme']['navigation_font_color'] ?? '#000000',
// Language
'current_lang' => $this->currentLanguage,
'current_lang_upper' => strtoupper($this->currentLanguage),
'available_langs' => $this->config['language']['available'] ?? ['nl', 'en'],
// Translations
't_home' => $this->t('home'),
't_search' => $this->t('search'),
't_search_placeholder' => $this->t('search_placeholder'),
't_search_button' => $this->t('search_button'),
't_welcome' => $this->t('welcome'),
't_created' => $this->t('created'),
't_modified' => $this->t('modified'),
't_author' => $this->t('author'),
't_manual' => $this->t('manual'),
't_no_content' => $this->t('no_content'),
't_no_results' => $this->t('no_results'),
't_results_found' => $this->t('results_found'),
't_breadcrumb_home' => $this->t('breadcrumb_home'),
't_file_details' => $this->t('file_details'),
't_guide' => $this->t('guide'),
't_powered_by' => $this->t('powered_by'),
't_directory_empty' => $this->t('directory_empty'),
't_page_not_found' => $this->t('page_not_found'),
't_page_not_found_text' => $this->t('page_not_found_text'),
't_mappen' => $this->t('mappen'),
't_paginas' => $this->t('paginas')
];
// File info for footer
if (isset($page['file_info'])) {
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info'] = $this->t('created') . ': ' . htmlspecialchars($page['file_info']['created']) .
' | ' . $this->t('modified') . ': ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
} else {
$templateData['file_info'] = '';
$templateData['file_info_block'] = '';
}
// File info for footer
if (isset($page['file_info'])) {
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
} else {
$templateData['file_info'] = '';
}
// Check if content exists for guide link
$hasContent = !$this->isContentDirEmpty();
@@ -796,60 +853,35 @@ class CodePressCMS {
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
// Load partials manually
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
if (!$hasContent) {
// Remove the link from header when no content
$headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent);
}
$footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
if (!$hasContent) {
// Remove guide link from footer when no content
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
}
// Load and render all templates with data
$layoutTemplate = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$headerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
$navigationTemplate = file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache');
$footerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
// Determine content type and load appropriate template
$contentType = $this->getContentType($page);
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
// Determine content type and load appropriate template
$pagePath = $_GET['page'] ?? $this->config['default_page'];
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
$filePath = $this->config['content_dir'] . '/' . $pagePath;
$contentType = 'markdown'; // default
if (file_exists($filePath . '.md')) {
$contentType = 'markdown';
} elseif (file_exists($filePath . '.php')) {
$contentType = 'php';
} elseif (file_exists($filePath . '.html')) {
$contentType = 'html';
}
// Render all templates with data
$renderedHeader = SimpleTemplate::render($headerTemplate, $templateData);
$renderedNavigation = SimpleTemplate::render($navigationTemplate, $templateData);
$renderedFooter = SimpleTemplate::render($footerTemplate, $templateData);
$renderedContent = SimpleTemplate::render($contentTemplate, $templateData);
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
// Replace partials in layout
$finalTemplate = str_replace('{{>header}}', $renderedHeader, $layoutTemplate);
$finalTemplate = str_replace('{{>navigation}}', $renderedNavigation, $finalTemplate);
$finalTemplate = str_replace('{{>footer}}', $renderedFooter, $finalTemplate);
$finalTemplate = str_replace('{{>content_template}}', $renderedContent, $finalTemplate);
$partials = [
'header' => file_get_contents($this->config['templates_dir'] . '/assets/header.mustache'),
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'),
'content_template' => $contentTemplate
];
// Render the final layout with all template data
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
// Replace partials in template
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
$template = str_replace('{{>header}}', $partials['header'], $template);
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
$template = str_replace('{{>footer}}', $partials['footer'], $template);
$template = str_replace('{{>content_template}}', $partials['content_template'], $template);
// Render template with data
$renderedTemplate = SimpleTemplate::render($template, $templateData);
echo $renderedTemplate;
echo $renderedLayout;
}
/**
@@ -857,30 +889,37 @@ class CodePressCMS {
*
* @return string Breadcrumb HTML
*/
private function getBreadcrumb() {
private function generateBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
// Start with home icon linking to default page (root)
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '"><i class="bi bi-house"></i></a></li>';
// Split page path and build breadcrumb items
$parts = explode('/', $page);
$currentPath = '';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$currentPath .= ($currentPath ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
// Last part - active page
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $title . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
// Parent directory - clickable link with separator
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="?page=' . $currentPath . '">' . $title . '</a></li>';
}
}
@@ -909,7 +948,7 @@ class CodePressCMS {
// Root level folders
$html .= '<li class="nav-item dropdown">';
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']);
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
@@ -919,7 +958,7 @@ class CodePressCMS {
// Nested folders in dropdown
$html .= '<li class="dropdown-submenu">';
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
$html .= htmlspecialchars($item['title']);
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
$html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);