Compare commits
2 commits
441c8bca68
...
92d681feb9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d681feb9 | ||
|
|
d10ff75aa4 |
2 changed files with 230 additions and 63 deletions
|
|
@ -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)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
[]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue