## 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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user