This commit is contained in:
2025-11-28 03:06:13 +01:00
parent b2be51d65e
commit 5de109b649
4 changed files with 273 additions and 363 deletions

View File

@@ -24,233 +24,14 @@ if (php_sapi_name() !== 'cli') {
}
}
// -----------------------------------------------------------
// Kleine Helper-Funktion für internes Logging + HTML-Debug
// -----------------------------------------------------------
function usb_i18n_debug_log(string $msg): void
{
// 1) In eine eigene Log-Datei schreiben (im config/-Ordner)
$logFile = __DIR__ . '/i18n_debug.log';
$line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
@file_put_contents($logFile, $line, FILE_APPEND);
require_once __DIR__ . '/i18n.php'; // <— NEU: zentrale Sprachlogik
// 2) optional auch ins PHP error_log
@error_log('[i18n] ' . $msg);
// 3) Bei ?debug_i18n=1 zusätzlich HTML-Kommentar ausgeben
if (isset($_GET['debug_i18n']) && $_GET['debug_i18n'] == '1') {
echo "<!-- [i18n] " . htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . " -->\n";
}
}
// -----------------------------------------------------------
// Browser-Sprache aus HTTP_ACCEPT_LANGUAGE bestimmen
// -----------------------------------------------------------
function usb_detect_browser_lang(array $availableLangs): ?string
{
if (empty($availableLangs)) {
usb_i18n_debug_log('Browser-Lang: keine availableLangs abbrechen');
return null;
}
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
usb_i18n_debug_log('HTTP_ACCEPT_LANGUAGE: ' . $header);
if ($header === '') {
return null;
}
$parts = explode(',', $header);
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
// Sprache vor dem ; nehmen (z.B. "de-DE", "en-US")
$langTag = strtolower(explode(';', $part)[0]);
if ($langTag === '') {
continue;
}
// 2-Buchstaben-Code extrahieren
$code2 = substr($langTag, 0, 2);
if (!preg_match('/^[a-z]{2}$/', $code2)) {
continue;
}
$exists = array_key_exists($code2, $availableLangs) ? 'yes' : 'no';
usb_i18n_debug_log("Browser-Lang-Kandidat: {$code2} (exists: {$exists})");
if (isset($availableLangs[$code2])) {
return $code2;
}
}
return null;
}
// -----------------------------------------------------------
// 1) Sprache aus ?lang lesen (nur 2-Buchstaben-Code zulassen)
// -----------------------------------------------------------
$requestedLang = $_GET['lang'] ?? null;
if (is_string($requestedLang)) {
$requestedLang = strtolower($requestedLang);
if (!preg_match('/^[a-z]{2}$/', $requestedLang)) {
$requestedLang = null;
}
} else {
$requestedLang = null;
}
usb_i18n_debug_log('requestedLang (GET): ' . var_export($requestedLang, true));
// -----------------------------------------------------------
// 2) Verfügbare JSON-Sprachen erkennen
// -----------------------------------------------------------
$i18nDir = __DIR__ . '/../public/assets/i18n';
$langFiles = [];
if (is_dir($i18nDir)) {
$langFiles = glob($i18nDir . '/*.json') ?: [];
}
$availableLangs = [];
// Alle vorhandenen JSONs einsammeln
foreach ($langFiles as $file) {
$raw = @file_get_contents($file);
if ($raw === false) {
usb_i18n_debug_log('Konnte Datei nicht lesen: ' . $file);
continue;
}
$json = json_decode($raw, true);
if (!is_array($json)) {
usb_i18n_debug_log('Ungültiges JSON in ' . $file . ' :: ' . json_last_error_msg());
continue;
}
$meta = $json['meta'] ?? [];
// Optional: nur Sprachen mit enabled=false ausblenden
if (array_key_exists('enabled', $meta) && $meta['enabled'] === false) {
usb_i18n_debug_log('Sprache deaktiviert (enabled=false) in ' . $file);
continue;
}
// Sprachcode bestimmen (immer 2-Buchstaben)
$code = strtolower($meta['code'] ?? basename($file, '.json'));
if (!preg_match('/^[a-z]{2}$/', $code)) {
// Sonderdateien (template.json etc.) ignorieren
usb_i18n_debug_log('Ignoriere Datei mit unpassendem Code: ' . $file);
continue;
}
$label = $meta['label'] ?? strtoupper($code);
$flag = $meta['flag'] ?? '🏳️';
$availableLangs[$code] = [
'code' => $code,
'label' => $label,
'flag' => $flag,
];
}
usb_i18n_debug_log('availableLangs keys: ' . implode(', ', array_keys($availableLangs)));
// Falls keine Sprachdateien gefunden wurden → Minimal-Fallback
if (empty($availableLangs)) {
usb_i18n_debug_log('WARN: keine Sprachdateien gefunden, fallback auf en');
$availableLangs = [
'en' => [
'code' => 'en',
'label' => 'English',
'flag' => '🏳️',
],
];
}
// -----------------------------------------------------------
// 3) Endgültige Sprache wählen nach deiner Priorität
// -----------------------------------------------------------
$lang = null;
// 1) ?lang=xx wird bevorzugt, wenn gültig + vorhanden
if ($requestedLang && isset($availableLangs[$requestedLang])) {
$lang = $requestedLang;
usb_i18n_debug_log('Auswahl: requestedLang übernommen: ' . $lang);
}
// 2) Sonst HTTP_ACCEPT_LANGUAGE (Browser), erste passende Sprache
if ($lang === null) {
$browserLang = usb_detect_browser_lang($availableLangs);
if ($browserLang !== null) {
$lang = $browserLang;
usb_i18n_debug_log('Auswahl: Browser-Lang übernommen: ' . $lang);
}
}
// 3) Wenn Browser-Sprache nicht existiert → 'en', wenn vorhanden
if ($lang === null && isset($availableLangs['en'])) {
$lang = 'en';
usb_i18n_debug_log('Auswahl: Fallback auf en, da kein Match');
}
// 4) Sonst: erste Sprache aus $availableLangs
if ($lang === null) {
$keys = array_keys($availableLangs);
$lang = $keys[0] ?? 'en';
usb_i18n_debug_log('Auswahl: Fallback auf erste Sprache: ' . $lang);
}
usb_i18n_debug_log('FINAL LANG: ' . $lang);
// -----------------------------------------------------------
// 4) Aktive Sprachdatei laden
// -----------------------------------------------------------
$activeLangFile = $i18nDir . '/' . $lang . '.json';
$activeLangData = [];
if (is_readable($activeLangFile)) {
$json = json_decode(@file_get_contents($activeLangFile), true);
if (is_array($json)) {
$activeLangData = $json;
} else {
usb_i18n_debug_log('Aktive JSON nicht array: ' . $activeLangFile);
}
} else {
usb_i18n_debug_log('Aktive Sprachdatei nicht lesbar: ' . $activeLangFile);
}
// -----------------------------------------------------------
// 5) Fallback-Sprache: immer EN, wenn vorhanden & nicht aktuell
// -----------------------------------------------------------
$fallbackLangData = [];
$fallbackFile = $i18nDir . '/en.json';
if ($lang !== 'en' && is_readable($fallbackFile)) {
$json = json_decode(@file_get_contents($fallbackFile), true);
if (is_array($json)) {
$fallbackLangData = $json;
} else {
usb_i18n_debug_log('Fallback-JSON (en) nicht array: ' . $fallbackFile);
}
}
// -----------------------------------------------------------
// 6) Globale i18n-Struktur bereitstellen
// -----------------------------------------------------------
$GLOBALS['lang'] = $lang;
$GLOBALS['availableLangs'] = $availableLangs;
$GLOBALS['i18n'] = [
'current' => $activeLangData,
'fallback' => $fallbackLangData,
// ab hier kannst du überall $GLOBALS['lang'] und $GLOBALS['availableLangs'] nutzen
// und für JS:
$usbConfig = [
// ... dein sonstiges Zeug ...
'i18n' => app_i18n_get_frontend_config(),
];
// -----------------------------------------------------------
// 7) Rest des Systems laden
// -----------------------------------------------------------

234
config/i18n.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
// config/i18n.php
// Zentrale Sprachlogik für das gesamte Projekt
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
/**
* Liest die meta-Infos einer Sprachdatei.
* Erwartet Struktur:
* {
* "meta": { "code": "de", "label": "Deutsch", "flag": "🇩🇪", "enabled": true },
* ...
* }
*
* Gibt NULL zurück, wenn:
* - Datei nicht lesbar
* - JSON ungültig
* - kein meta vorhanden
* - meta.enabled existiert und NICHT true ist
*/
function app_i18n_load_language_meta_from_file(string $file): ?array
{
$json = @file_get_contents($file);
if ($json === false) {
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
return null;
}
$meta = $data['meta'] ?? null;
if (!is_array($meta)) {
return null;
}
// Nur aktive Sprachen (enabled === true)
if (array_key_exists('enabled', $meta) && !$meta['enabled']) {
return null;
}
// Code aus meta, Fallback: Dateiname
$code = '';
if (!empty($meta['code'])) {
$code = strtolower(substr((string)$meta['code'], 0, 5));
} else {
$base = basename($file, '.json');
$code = strtolower($base);
}
// Auf 2-Buchstaben-Codes normalisieren (de-DE → de)
if (strlen($code) > 2) {
$code = substr($code, 0, 2);
}
if ($code === '') {
return null;
}
$label = isset($meta['label']) && $meta['label'] !== ''
? (string)$meta['label']
: strtoupper($code);
$flag = isset($meta['flag']) ? (string)$meta['flag'] : '';
return [
'code' => $code,
'label' => $label,
'flag' => $flag,
];
}
/**
* Alle verfügbaren Sprachen aus /public/assets/i18n/*.json ermitteln.
* Verfügbar = JSON mit meta.enabled === true.
* EN wird garantiert hinzugefügt (Fallback), falls nicht gefunden.
*/
function app_i18n_detect_available_languages(): array
{
$baseDir = realpath(__DIR__ . '/../public/assets/i18n');
if ($baseDir === false) {
// Wenn gar kein Verzeichnis da ist: minimaler EN-Fallback
return [
'en' => [
'code' => 'en',
'label' => 'English',
'flag' => '',
],
];
}
$files = glob($baseDir . '/*.json') ?: [];
$langs = [];
foreach ($files as $file) {
$meta = app_i18n_load_language_meta_from_file($file);
if ($meta === null) {
continue;
}
$code = $meta['code'];
// Erste gültige Definition pro Code gewinnt
if (!isset($langs[$code])) {
$langs[$code] = $meta;
}
}
// EN muss immer vorhanden sein (laut deiner Vorgabe)
if (!isset($langs['en'])) {
// Versuch: gibt es eine en.json, auch wenn enabled=false?
foreach ($files as $file) {
$base = strtolower(basename($file, '.json'));
if ($base === 'en') {
$json = @file_get_contents($file);
$data = json_decode($json, true);
$meta = is_array($data['meta'] ?? null) ? $data['meta'] : [];
$label = isset($meta['label']) && $meta['label'] !== ''
? (string)$meta['label']
: 'English';
$flag = isset($meta['flag']) ? (string)$meta['flag'] : '';
$langs['en'] = [
'code' => 'en',
'label' => $label,
'flag' => $flag,
];
break;
}
}
}
// Wenn immer noch kein EN → minimaler Stub
if (!isset($langs['en'])) {
$langs['en'] = [
'code' => 'en',
'label' => 'English',
'flag' => '',
];
}
ksort($langs);
return $langs;
}
/**
* Browsersprache aus HTTP_ACCEPT_LANGUAGE extrahieren (2-Buchstaben),
* aber nur, wenn sie in $available existiert.
*/
function app_i18n_detect_browser_lang(array $available): ?string
{
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if ($header === '') {
return null;
}
$parts = explode(',', $header);
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$code = strtolower(substr($part, 0, 2)); // "de-DE" → "de"
if (isset($available[$code])) {
return $code;
}
}
return null;
}
/**
* Aktuelle Sprache bestimmen:
* 1) ?lang=xx (wenn in $available)
* 2) Browsersprache (wenn in $available)
* 3) Fallback "en"
*/
function app_i18n_resolve_current_lang(array $available): string
{
// 1) URL-Parameter ?lang=xx
if (!empty($_GET['lang'])) {
$param = strtolower(substr($_GET['lang'], 0, 2));
if (isset($available[$param])) {
return $param;
}
}
// 2) Browsersprache
$browser = app_i18n_detect_browser_lang($available);
if ($browser !== null) {
return $browser;
}
// 3) Standard EN
if (isset($available['en'])) {
return 'en';
}
// Sicherheitsfallback: erste verfügbare Sprache
$keys = array_keys($available);
return $keys[0] ?? 'en';
}
// -----------------------------------------------------
// Bootstrap ausführen
// -----------------------------------------------------
$availableLangs = app_i18n_detect_available_languages();
$currentLang = app_i18n_resolve_current_lang($availableLangs);
// Global bereitstellen
$GLOBALS['availableLangs'] = $availableLangs;
$GLOBALS['lang'] = $currentLang;
// Optional in Session merken (muss nicht, schadet aber auch nicht)
$_SESSION['lang'] = $currentLang;
/**
* Frontend-Config für JS (usbConfig.i18n)
* → benutzt direkt die Meta-Daten aus den JSONs (code, label, flag)
*/
function app_i18n_get_frontend_config(): array
{
return [
'current' => $GLOBALS['lang'] ?? 'en',
'available' => $GLOBALS['availableLangs'] ?? [],
];
}