= 3) { // Pattern: name.lang.ext $fileLang = $parts[count($parts) - 2]; if (in_array($fileLang, $availableLangs)) { // Only include if it matches current language if ($fileLang === $lang) { $contentFiles[] = [ 'path' => $filePath, 'name' => $file, 'sort_key' => $parts[0] ]; } continue; } } // Default files (no language suffix) - include if no language-specific version exists $baseName = $parts[0]; $hasLangVersion = false; if ($lang !== $defaultLang) { // Check if language-specific version exists foreach (CONTENT_EXTENSIONS as $checkExt) { if (file_exists("$dir/$baseName.$lang.$checkExt")) { $hasLangVersion = true; break; } } } if (!$hasLangVersion) { $contentFiles[] = [ 'path' => $filePath, 'name' => $file, 'sort_key' => $baseName ]; } } // Sort by filename (alphanumerical) usort($contentFiles, fn($a, $b) => strnatcmp($a['sort_key'], $b['sort_key'])); return array_column($contentFiles, 'path'); } function resolveTranslatedPath(Context $ctx, string $requestPath): string { // If default language, no translation needed if ($ctx->currentLang === $ctx->defaultLang) { return $requestPath; } $parts = explode('/', trim($requestPath, '/')); $resolvedParts = []; $currentPath = $ctx->contentDir; foreach ($parts as $segment) { if (empty($segment)) continue; // Check all subdirectories for slug matches $found = false; if (is_dir($currentPath)) { $subdirs = getSubdirectories($currentPath); foreach ($subdirs as $dir) { $metadata = loadMetadata("$currentPath/$dir", $ctx->currentLang, $ctx->defaultLang); if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) { $resolvedParts[] = $dir; $currentPath .= "/$dir"; $found = true; break; } } } // If no slug match, use segment as-is if (!$found) { $resolvedParts[] = $segment; $currentPath .= "/$segment"; } } return implode('/', $resolvedParts); } function parseRequestPath(Context $ctx): array { // Resolve translated slugs to actual directory names $resolvedPath = resolveTranslatedPath($ctx, $ctx->requestPath); $contentPath = rtrim($ctx->contentDir, '/') . '/' . ltrim($resolvedPath, '/'); if (is_file($contentPath)) { return ['type' => 'file', 'path' => realpath($contentPath)]; } if (is_dir($contentPath)) { // Check if directory has subdirectories (PHP 8.4: cleaner with array_any later) $hasSubdirs = !empty(getSubdirectories($contentPath)); // If directory has subdirectories, it's an article-type folder (list view) if ($hasSubdirs) { return ['type' => 'directory', 'path' => realpath($contentPath)]; } // No subdirectories - it's a page-type folder // Find all content files in this directory $contentFiles = findAllContentFiles($contentPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs); if (!empty($contentFiles)) { return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$ctx->hasTrailingSlash]; } // No content files found return ['type' => 'directory', 'path' => realpath($contentPath)]; } return ['type' => 'not_found', 'path' => $contentPath]; } function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array { $metadataFile = "$dirPath/metadata.ini"; if (!file_exists($metadataFile)) return null; $metadata = parse_ini_file($metadataFile, true); if (!$metadata) return null; // Extract base metadata (non-section values) $baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY); // If current language is not default, merge language-specific overrides if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) { $baseMetadata = array_merge($baseMetadata, $metadata[$lang]); } return $baseMetadata ?: null; } function buildNavigation(Context $ctx): array { $navItems = []; $items = getSubdirectories($ctx->contentDir); foreach ($items as $item) { $itemPath = "{$ctx->contentDir}/$item"; $metadata = loadMetadata($itemPath, $ctx->currentLang, $ctx->defaultLang); // Check if this item should be in menu if (!$metadata || empty($metadata['menu'])) { continue; } // Check if content exists for current language if ($ctx->currentLang !== $ctx->defaultLang && shouldHideUntranslated()) { $hasLangContent = hasLanguageContent($itemPath, $ctx->currentLang, CONTENT_EXTENSIONS); $hasLangMetadata = hasLanguageMetadata($itemPath, $ctx->currentLang); if (!$hasLangContent && !$hasLangMetadata) continue; } // Extract title and build URL $title = $metadata['title'] ?? extractTitle($itemPath, $ctx->currentLang, $ctx->defaultLang) ?? ucfirst($item); // Use translated slug if available $urlSlug = ($ctx->currentLang !== $ctx->defaultLang && $metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item; $navItems[] = [ 'title' => $title, 'url' => $ctx->langPrefix . '/' . urlencode($urlSlug) . '/', 'order' => (int)($metadata['menu_order'] ?? 999) ]; } // Sort by menu_order usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']); return $navItems; }