Compare commits

...

2 commits

Author SHA1 Message Date
Ruben
92d681feb9 Add URL-based i18n support to language plugin
Extract language from URL and store in context Filter content and
metadata by language Add language variables to templates Implement
language-specific file filtering Add date formatting with translated
months Generate language-specific URLs
2025-11-25 23:16:45 +01:00
Ruben
d10ff75aa4 Add slug resolution for content paths
Implement function to resolve slugs to actual folder paths Update path
parsing to handle slug resolution Add language prefix support to
navigation URLs
2025-11-25 23:16:34 +01:00
2 changed files with 230 additions and 63 deletions

View file

@ -32,6 +32,28 @@ function findAllContentFiles(string $dir): array {
return array_column($contentFiles, 'path'); return array_column($contentFiles, 'path');
} }
function resolveSlugToFolder(string $parentDir, string $slug): ?string {
if (!is_dir($parentDir)) return null;
$items = scandir($parentDir) ?: [];
foreach ($items as $item) {
if ($item === '.' || $item === '..' || !is_dir("$parentDir/$item")) continue;
// Check if folder name matches slug
if ($item === $slug) {
return $item;
}
// Check metadata for custom slug
$metadata = loadMetadata("$parentDir/$item");
if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $slug) {
return $item;
}
}
return null;
}
function parseRequestPath(Context $ctx): array { function parseRequestPath(Context $ctx): array {
$requestPath = $ctx->requestPath; $requestPath = $ctx->requestPath;
@ -39,7 +61,20 @@ function parseRequestPath(Context $ctx): array {
return ['type' => 'frontpage', 'path' => $ctx->contentDir]; return ['type' => 'frontpage', 'path' => $ctx->contentDir];
} }
$contentPath = $ctx->contentDir . '/' . $requestPath; // Try resolving slug to actual folder path
$pathParts = explode('/', trim($requestPath, '/'));
$resolvedPath = $ctx->contentDir;
foreach ($pathParts as $part) {
$resolved = resolveSlugToFolder($resolvedPath, $part);
if ($resolved === null) {
// Slug not found, return not_found
return ['type' => 'not_found', 'path' => $ctx->contentDir . '/' . $requestPath];
}
$resolvedPath .= '/' . $resolved;
}
$contentPath = $resolvedPath;
// Check if it's a directory // Check if it's a directory
if (is_dir($contentPath)) { if (is_dir($contentPath)) {
@ -78,6 +113,7 @@ function loadMetadata(string $dirPath): ?array {
function buildNavigation(Context $ctx): array { function buildNavigation(Context $ctx): array {
$items = scandir($ctx->contentDir) ?: []; $items = scandir($ctx->contentDir) ?: [];
$navItems = []; $navItems = [];
$langPrefix = $ctx->get('langPrefix', '');
foreach ($items as $item) { foreach ($items as $item) {
if ($item === '.' || $item === '..' || !is_dir($ctx->contentDir . "/$item")) continue; if ($item === '.' || $item === '..' || !is_dir($ctx->contentDir . "/$item")) continue;
@ -99,7 +135,7 @@ function buildNavigation(Context $ctx): array {
$navItems[] = [ $navItems[] = [
'title' => $title, 'title' => $title,
'url' => '/' . urlencode($urlSlug) . '/', 'url' => $langPrefix . '/' . urlencode($urlSlug) . '/',
'order' => (int)($metadata['menu_order'] ?? 999) 'order' => (int)($metadata['menu_order'] ?? 999)
]; ];
} }

View file

@ -1,85 +1,216 @@
<?php <?php
// Languages plugin - translation loading and filtering // Language plugin - URL-based i18n support
function loadTranslations(string $lang): array { // Extract language from URL, store in context
$defaultTranslationFile = dirname(__DIR__, 2) . "/default/languages/$lang.ini"; Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
$customTranslationFile = dirname(__DIR__, 3) . "/custom/languages/$lang.ini"; $defaultLang = $config['languages']['default'] ?? 'en';
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'en'));
$translations = []; $pathParts = explode('/', $ctx->requestPath);
$currentLang = $defaultLang;
if (file_exists($defaultTranslationFile)) { // Remove language prefix from URL if present
$translations = parse_ini_file($defaultTranslationFile) ?: []; if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) {
$currentLang = array_shift($pathParts);
$reflection = new ReflectionProperty($ctx, 'requestPath');
$reflection->setValue($ctx, implode('/', $pathParts));
} }
if (file_exists($customTranslationFile)) { $ctx->set('currentLang', $currentLang);
$customTranslations = parse_ini_file($customTranslationFile) ?: []; $ctx->set('defaultLang', $defaultLang);
$translations = array_merge($translations, $customTranslations); $ctx->set('availableLangs', $availableLangs);
$ctx->set('langPrefix', $currentLang !== $defaultLang ? "/$currentLang" : '');
$ctx->set('translations', loadTranslations($currentLang));
return $ctx;
});
// Filter content and metadata by language
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dirOrType, string $extraContext = '') {
global $GLOBALS;
$ctx = $GLOBALS['ctx'] ?? null;
if (!$ctx) return $data;
$currentLang = $ctx->get('currentLang', 'en');
// Merge language-specific metadata sections
if ($extraContext === 'metadata' && isset($data['_raw'][$currentLang]) && is_array($data['_raw'][$currentLang])) {
return array_merge($data, $data['_raw'][$currentLang]);
}
// Format dates with translated month names
if ($dirOrType === 'date_format') {
return formatDate($data, $currentLang);
}
// Filter content files by language variant
if (is_array($data) && !empty($data) && isset($data[0]['path'])) {
return filterFilesByLanguage($data, $dirOrType, $ctx);
}
return $data;
});
// Add language variables to templates
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
$currentLang = $ctx->get('currentLang', 'en');
$defaultLang = $ctx->get('defaultLang', 'en');
$availableLangs = $ctx->get('availableLangs', ['en']);
$vars['currentLang'] = $currentLang;
$vars['defaultLang'] = $defaultLang;
$vars['langPrefix'] = $ctx->get('langPrefix', '');
$vars['translations'] = $ctx->get('translations', []);
$vars['availableLangs'] = $availableLangs;
$vars['languageUrls'] = buildLanguageUrls($ctx, $currentLang, $defaultLang, $availableLangs);
return $vars;
});
// --- Helper functions ---
function loadTranslations(string $lang): array {
$defaultFile = dirname(__DIR__, 2) . "/default/languages/$lang.ini";
$customFile = dirname(__DIR__, 3) . "/custom/languages/$lang.ini";
$translations = file_exists($defaultFile) ? parse_ini_file($defaultFile) ?: [] : [];
if (file_exists($customFile)) {
$translations = array_merge($translations, parse_ini_file($customFile) ?: []);
} }
return $translations; return $translations;
} }
function shouldHideUntranslated(): bool {
$configFile = file_exists(__DIR__ . '/../../../custom/config.ini')
? __DIR__ . '/../../../custom/config.ini'
: __DIR__ . '/../../config.ini';
if (!file_exists($configFile)) return true;
$config = parse_ini_file($configFile, true);
return !isset($config['languages']['show_untranslated'])
|| $config['languages']['show_untranslated'] !== 'true';
}
function hasLanguageMetadata(string $dirPath, string $lang): bool {
$metadataFile = "$dirPath/metadata.ini";
if (!file_exists($metadataFile)) return false;
$metadata = parse_ini_file($metadataFile, true);
if (!$metadata) return false;
if (!isset($metadata[$lang]) || !is_array($metadata[$lang])) return false;
// Check if language section has meaningful content (title or summary)
return isset($metadata[$lang]['title']) || isset($metadata[$lang]['summary']);
}
function hasLanguageContent(string $dirPath, string $lang, array $contentExtensions): bool {
if (!is_dir($dirPath)) return false;
$files = scandir($dirPath) ?: [];
foreach ($files as $file) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
if (!in_array($ext, $contentExtensions)) continue;
$parts = explode('.', $file);
if (count($parts) >= 3) {
$fileLang = $parts[count($parts) - 2];
if ($fileLang === $lang && is_file("$dirPath/$file")) {
return true;
}
}
}
return false;
}
function formatDate(string $dateString, string $lang): string { function formatDate(string $dateString, string $lang): string {
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) { if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $m)) {
return $dateString; return $dateString;
} }
$translations = loadTranslations($lang); $translations = loadTranslations($lang);
$day = (int)$matches[3]; $day = (int)$m[3];
$monthIndex = (int)$matches[2] - 1; $monthIndex = (int)$m[2] - 1;
$year = $matches[1]; $year = $m[1];
$month = $m[2];
if (isset($translations['months'])) { if (isset($translations['months'])) {
$months = array_map('trim', explode(',', $translations['months'])); $months = array_map('trim', explode(',', $translations['months']));
$month = $months[$monthIndex] ?? $matches[2]; $month = $months[$monthIndex] ?? $m[2];
} else {
$month = $matches[2];
} }
return "$day. $month $year"; return "$day. $month $year";
} }
function filterFilesByLanguage(array $files, string $dir, Context $ctx): array {
$currentLang = $ctx->get('currentLang', 'en');
$defaultLang = $ctx->get('defaultLang', 'en');
$availableLangs = $ctx->get('availableLangs', ['en']);
$filtered = [];
$seen = [];
foreach ($files as $file) {
$parts = explode('.', $file['name']);
// Language-specific file (name.lang.ext)
if (count($parts) >= 3 && in_array($parts[count($parts) - 2], $availableLangs)) {
$fileLang = $parts[count($parts) - 2];
if ($fileLang === $currentLang) {
$filtered[] = $file;
$seen[$parts[0]] = true;
}
continue;
}
// Default file - include if no language version exists
$baseName = $parts[0];
if (!isset($seen[$baseName])) {
$hasLangVersion = $currentLang !== $defaultLang &&
array_reduce(CONTENT_EXTENSIONS,
fn($found, $ext) => $found || file_exists("$dir/$baseName.$currentLang.$ext"),
false);
if (!$hasLangVersion) {
$filtered[] = $file;
$seen[$baseName] = true;
}
}
}
return $filtered;
}
function buildLanguageUrls(Context $ctx, string $currentLang, string $defaultLang, array $availableLangs): array {
// Frontpage URLs
if (empty($ctx->requestPath)) {
return array_reduce($availableLangs,
fn($urls, $lang) => $urls + [$lang => $lang === $defaultLang ? '/' : "/$lang/"],
[]);
}
// Resolve current path to actual folder names
$folders = resolvePath($ctx->requestPath, $ctx->contentDir);
if (!$folders) {
// Fallback: simple language prefix
return buildSimpleUrls($ctx, $defaultLang, $availableLangs);
}
// Build URLs with language-specific slugs
$urls = [];
foreach ($availableLangs as $lang) {
$segments = [];
$dir = $ctx->contentDir;
foreach ($folders as $folderName) {
$metadataFile = "$dir/$folderName/metadata.ini";
$slug = $folderName;
if (file_exists($metadataFile)) {
$metadata = parse_ini_file($metadataFile, true) ?: [];
// Check for language-specific slug
if (isset($metadata[$lang]['slug'])) {
$slug = $metadata[$lang]['slug'];
} elseif (isset($metadata['slug'])) {
$slug = $metadata['slug'];
}
}
$segments[] = $slug;
$dir .= '/' . $folderName;
}
$path = '/' . implode('/', $segments);
if ($ctx->hasTrailingSlash) $path .= '/';
$urls[$lang] = $lang === $defaultLang ? $path : "/$lang" . $path;
}
return $urls;
}
function resolvePath(string $path, string $contentDir): ?array {
$parts = explode('/', trim($path, '/'));
$folders = [];
$dir = $contentDir;
foreach ($parts as $slug) {
$folderName = resolveSlugToFolder($dir, $slug);
if (!$folderName) return null;
$folders[] = $folderName;
$dir .= '/' . $folderName;
}
return $folders;
}
function buildSimpleUrls(Context $ctx, string $defaultLang, array $availableLangs): array {
$path = '/' . $ctx->requestPath;
if ($ctx->hasTrailingSlash) $path .= '/';
return array_reduce($availableLangs,
fn($urls, $lang) => $urls + [$lang => $lang === $defaultLang ? $path : "/$lang" . $path],
[]);
}