Compare commits

..

No commits in common. "92d681feb9ebd620021abdcbaf1e77ad92a18fb5" and "441c8bca686b90d9f3add1c9fd628442a17ab19d" have entirely different histories.

2 changed files with 64 additions and 231 deletions

View file

@ -32,28 +32,6 @@ 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;
@ -61,20 +39,7 @@ function parseRequestPath(Context $ctx): array {
return ['type' => 'frontpage', 'path' => $ctx->contentDir]; return ['type' => 'frontpage', 'path' => $ctx->contentDir];
} }
// Try resolving slug to actual folder path $contentPath = $ctx->contentDir . '/' . $requestPath;
$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)) {
@ -113,7 +78,6 @@ 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;
@ -135,7 +99,7 @@ function buildNavigation(Context $ctx): array {
$navItems[] = [ $navItems[] = [
'title' => $title, 'title' => $title,
'url' => $langPrefix . '/' . urlencode($urlSlug) . '/', 'url' => '/' . urlencode($urlSlug) . '/',
'order' => (int)($metadata['menu_order'] ?? 999) 'order' => (int)($metadata['menu_order'] ?? 999)
]; ];
} }

View file

@ -1,216 +1,85 @@
<?php <?php
// Language plugin - URL-based i18n support // Languages plugin - translation loading and filtering
// Extract language from URL, store in context
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
$defaultLang = $config['languages']['default'] ?? 'en';
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'en'));
$pathParts = explode('/', $ctx->requestPath);
$currentLang = $defaultLang;
// Remove language prefix from URL if present
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));
}
$ctx->set('currentLang', $currentLang);
$ctx->set('defaultLang', $defaultLang);
$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 { function loadTranslations(string $lang): array {
$defaultFile = dirname(__DIR__, 2) . "/default/languages/$lang.ini"; $defaultTranslationFile = dirname(__DIR__, 2) . "/default/languages/$lang.ini";
$customFile = dirname(__DIR__, 3) . "/custom/languages/$lang.ini"; $customTranslationFile = dirname(__DIR__, 3) . "/custom/languages/$lang.ini";
$translations = file_exists($defaultFile) ? parse_ini_file($defaultFile) ?: [] : []; $translations = [];
if (file_exists($customFile)) { if (file_exists($defaultTranslationFile)) {
$translations = array_merge($translations, parse_ini_file($customFile) ?: []); $translations = parse_ini_file($defaultTranslationFile) ?: [];
}
if (file_exists($customTranslationFile)) {
$customTranslations = parse_ini_file($customTranslationFile) ?: [];
$translations = array_merge($translations, $customTranslations);
} }
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, $m)) { if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) {
return $dateString; return $dateString;
} }
$translations = loadTranslations($lang); $translations = loadTranslations($lang);
$day = (int)$m[3]; $day = (int)$matches[3];
$monthIndex = (int)$m[2] - 1; $monthIndex = (int)$matches[2] - 1;
$year = $m[1]; $year = $matches[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] ?? $m[2]; $month = $months[$monthIndex] ?? $matches[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],
[]);
}