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
This commit is contained in:
Ruben 2025-11-25 23:16:45 +01:00
parent d10ff75aa4
commit 92d681feb9

View file

@ -1,85 +1,216 @@
<?php
// Languages plugin - translation loading and filtering
// Language plugin - URL-based i18n support
function loadTranslations(string $lang): array {
$defaultTranslationFile = dirname(__DIR__, 2) . "/default/languages/$lang.ini";
$customTranslationFile = dirname(__DIR__, 3) . "/custom/languages/$lang.ini";
// 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'));
$translations = [];
$pathParts = explode('/', $ctx->requestPath);
$currentLang = $defaultLang;
if (file_exists($defaultTranslationFile)) {
$translations = parse_ini_file($defaultTranslationFile) ?: [];
// 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));
}
if (file_exists($customTranslationFile)) {
$customTranslations = parse_ini_file($customTranslationFile) ?: [];
$translations = array_merge($translations, $customTranslations);
$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 {
$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;
}
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 {
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) {
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $m)) {
return $dateString;
}
$translations = loadTranslations($lang);
$day = (int)$matches[3];
$monthIndex = (int)$matches[2] - 1;
$year = $matches[1];
$day = (int)$m[3];
$monthIndex = (int)$m[2] - 1;
$year = $m[1];
$month = $m[2];
if (isset($translations['months'])) {
$months = array_map('trim', explode(',', $translations['months']));
$month = $months[$monthIndex] ?? $matches[2];
} else {
$month = $matches[2];
$month = $months[$monthIndex] ?? $m[2];
}
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],
[]);
}