From a2b7fcb1a8d2c57b1d81ad78b377e57ecb7c8d40 Mon Sep 17 00:00:00 2001 From: Edwin Noorlander Date: Sat, 22 Nov 2025 15:29:47 +0100 Subject: [PATCH] ## 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. --- config.json | 14 +- engine/core/class/CodePressCMS.php | 249 +++++++++++--------- engine/lang/en.php | 26 ++ engine/lang/nl.php | 26 ++ engine/templates/assets/footer.mustache | 4 +- engine/templates/assets/header.mustache | 48 +++- engine/templates/assets/navigation.mustache | 4 +- engine/templates/layout.mustache | 127 ++++++++++ public/assets/css/mobile.css | 54 +++++ 9 files changed, 431 insertions(+), 121 deletions(-) create mode 100644 engine/lang/en.php create mode 100644 engine/lang/nl.php create mode 100644 public/assets/css/mobile.css diff --git a/config.json b/config.json index b8f56f1..b3b9f43 100644 --- a/config.json +++ b/config.json @@ -1,12 +1,18 @@ { "site_title": "CodePress", - "content_dir": "public/content", + "content_dir": "content", "templates_dir": "engine/templates", "default_page": "welkom", "theme": { - "primary_color": "#0d6efd", - "navbar_style": "bg-primary" + "header_color": "#0a369d", + "header_font_color": "#ffffff", + "navigation_color": "#2754b4", + "navigation_font_color": "#ffffff" + }, + "language": { + "default": "nl", + "available": ["nl", "en"] }, "seo": { "description": "CodePress CMS - Lightweight file-based content management system", @@ -22,4 +28,4 @@ "search_enabled": true, "breadcrumbs_enabled": true } -} \ No newline at end of file +} diff --git a/engine/core/class/CodePressCMS.php b/engine/core/class/CodePressCMS.php index f7a670a..245a5ea 100644 --- a/engine/core/class/CodePressCMS.php +++ b/engine/core/class/CodePressCMS.php @@ -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 = '

Search Results for: "' . htmlspecialchars($query) . '"

'; + $content = '

' . $this->t('search') . ' ' . $this->t('results_found') . ': "' . htmlspecialchars($query) . '"

'; if (empty($this->searchResults)) { - $content .= '

No results found.

'; + $content .= '

' . $this->t('no_results') . '.

'; } else { - $content .= '

Found ' . count($this->searchResults) . ' results:

'; + $content .= '

' . count($this->searchResults) . ' ' . $this->t('results_found') . ':

'; foreach ($this->searchResults as $result) { $content .= '
'; $content .= '
'; @@ -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 .= '
'; - - // 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 .= '
'; - $content .= '

📁 Mappen

'; + // Display all items in a single column + if (!empty($allItems)) { $content .= '
'; - $hasContent = true; - } - - // Display files - if (!empty($files)) { - $content .= '
'; - $content .= '

📄 Pagina\'s

'; - $content .= '
'; - foreach ($files as $file) { - $icon = $file['type'] === 'md' ? 'bi-file-text' : ($file['type'] === 'php' ? 'bi-file-code' : 'bi-file-earmark'); - $content .= ''; - $content .= ' ' . htmlspecialchars($file['name']); - $content .= ''; - } - $content .= '
'; + $content .= '
'; $hasContent = true; } $content .= '
'; if (!$hasContent) { - $content .= '

Deze map is leeg.

'; + $content .= '

' . $this->t('directory_empty') . '.

'; } return [ @@ -726,8 +756,8 @@ class CodePressCMS { */ private function getError404() { return [ - 'title' => 'Page Not Found', - 'content' => '

404 - Page Not Found

The page you are looking for does not exist.

' + 'title' => $this->t('page_not_found'), + 'content' => '

404 - ' . $this->t('page_not_found') . '

' . $this->t('page_not_found_text') . '

' ]; } @@ -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'] = ' | ' . $templateData['file_info'] . ''; } 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('/\s*]*>(.*?)<\/h1>\s*<\/a>/', '

$1

', $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('/\s*\|\s*
]*>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) : '
{{{content}}}
'; - // 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) : '
{{{content}}}
'; + // 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 '
'; + return ''; } $page = $_GET['page'] ?? $this->config['default_page']; $page = preg_replace('/\.[^.]+$/', '', $page); if ($page === $this->config['default_page']) { - return ''; + return ''; } - $parts = explode('/', $page); - $breadcrumb = '
- + | - Powered by CodePress CMS + {{t_powered_by}} CodePress CMS
diff --git a/engine/templates/assets/header.mustache b/engine/templates/assets/header.mustache index bc2a2f1..f643277 100644 --- a/engine/templates/assets/header.mustache +++ b/engine/templates/assets/header.mustache @@ -1,18 +1,50 @@ -