2025-11-11 23:36:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// Language plugin - URL-based i18n support
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// 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'));
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
$pathParts = explode('/', $ctx->requestPath);
|
|
|
|
|
$currentLang = $defaultLang;
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// 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));
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
$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]);
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// 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'])) {
|
2025-11-27 21:29:35 +01:00
|
|
|
error_log("filterFilesByLanguage called with " . count($data) . " files, current lang: $currentLang");
|
|
|
|
|
$filtered = filterFilesByLanguage($data, $dirOrType, $ctx);
|
|
|
|
|
error_log("Filtered to " . count($filtered) . " files");
|
|
|
|
|
return $filtered;
|
2025-11-25 23:16:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
});
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// 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']);
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
$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);
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
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;
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
function formatDate(string $dateString, string $lang): string {
|
|
|
|
|
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $m)) {
|
|
|
|
|
return $dateString;
|
|
|
|
|
}
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
$translations = loadTranslations($lang);
|
|
|
|
|
$day = (int)$m[3];
|
|
|
|
|
$monthIndex = (int)$m[2] - 1;
|
|
|
|
|
$year = $m[1];
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
$month = $m[2];
|
|
|
|
|
if (isset($translations['months'])) {
|
|
|
|
|
$months = array_map('trim', explode(',', $translations['months']));
|
|
|
|
|
$month = $months[$monthIndex] ?? $m[2];
|
|
|
|
|
}
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
return "$day. $month $year";
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
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 = [];
|
2025-11-11 23:36:53 +01:00
|
|
|
|
|
|
|
|
foreach ($files as $file) {
|
2025-11-25 23:16:45 +01:00
|
|
|
$parts = explode('.', $file['name']);
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// Language-specific file (name.lang.ext)
|
|
|
|
|
if (count($parts) >= 3 && in_array($parts[count($parts) - 2], $availableLangs)) {
|
2025-11-11 23:36:53 +01:00
|
|
|
$fileLang = $parts[count($parts) - 2];
|
2025-11-25 23:16:45 +01:00
|
|
|
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;
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-25 23:16:45 +01:00
|
|
|
|
|
|
|
|
return $filtered;
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
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/"],
|
|
|
|
|
[]);
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// Resolve current path to actual folder names
|
|
|
|
|
$folders = resolvePath($ctx->requestPath, $ctx->contentDir);
|
|
|
|
|
if (!$folders) {
|
|
|
|
|
// Fallback: simple language prefix
|
|
|
|
|
return buildSimpleUrls($ctx, $defaultLang, $availableLangs);
|
|
|
|
|
}
|
2025-11-11 23:36:53 +01:00
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
// 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;
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 23:16:45 +01:00
|
|
|
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],
|
|
|
|
|
[]);
|
2025-11-11 23:36:53 +01:00
|
|
|
}
|