Files
usbcheck.it/config/i18n.php
2025-11-29 00:30:50 +01:00

377 lines
9.3 KiB
PHP

<?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'] ?? [],
];
}
/**
* Komplette JSON-Struktur für eine Sprache laden.
* Nutzt einfachen Request-Cache, damit pro Sprache nur einmal von Platte gelesen wird.
*/
function app_i18n_load_lang_json(string $lang): array
{
static $cache = [];
$lang = strtolower(substr($lang, 0, 5));
if (isset($cache[$lang])) {
return $cache[$lang];
}
$baseDir = realpath(__DIR__ . '/../public/assets/i18n');
if ($baseDir === false) {
$cache[$lang] = [];
return $cache[$lang];
}
$path = $baseDir . '/' . $lang . '.json';
if (!is_file($path)) {
// Fallback: en.json, falls vorhanden
$fallback = $baseDir . '/en.json';
if (is_file($fallback)) {
$json = @file_get_contents($fallback);
$data = json_decode($json, true);
$cache[$lang] = is_array($data) ? $data : [];
return $cache[$lang];
}
$cache[$lang] = [];
return $cache[$lang];
}
$json = @file_get_contents($path);
$data = json_decode($json, true);
$cache[$lang] = is_array($data) ? $data : [];
return $cache[$lang];
}
/**
* Nav-Anker für eine Seite aus der Sprachdatei holen.
*
* Unterstützte Varianten im JSON:
*
* Variante A (direkte Labels):
* "pages": {
* "landing": {
* "anchors": {
* "how": "So funktioniert USBCheck",
* "problem": "Warum gefälschte USB-Sticks gefährlich sind"
* }
* }
* }
*
* Variante B (i18n-Keys):
* "pages": {
* "landing": {
* "anchors": {
* "how": "nav_how",
* "problem": "nav_problem"
* }
* }
* }
*
* Variante C (explizit):
* "pages": {
* "landing": {
* "anchors": {
* "how": { "label": "So funktioniert USBCheck", "i18n": "nav_how" },
* "problem": { "i18n": "nav_problem" }
* }
* }
* }
*
* Rückgabe-Format:
* [
* [ 'href' => '#how', 'label' => 'So funktioniert USBCheck', 'i18n' => '' ],
* [ 'href' => '#problem', 'label' => '', 'i18n' => 'nav_problem' ],
* ]
*/
function app_get_nav_anchors(string $pageKey): array
{
$lang = $GLOBALS['lang'] ?? 'en';
$data = app_i18n_load_lang_json($lang);
$cfg = $data['pages'][$pageKey]['anchors'] ?? null;
if (!is_array($cfg)) {
return [];
}
$anchors = [];
foreach ($cfg as $id => $value) {
$id = trim((string)$id);
if ($id === '') {
continue;
}
$href = '#' . $id;
$label = null;
$i18n = null;
if (is_string($value)) {
$valueTrim = trim($value);
if ($valueTrim !== '') {
// Heuristik:
// - enthält der Wert Leerzeichen → fertiges Label
// - sonst → i18n-Key (z.B. "nav_how")
if (preg_match('/\s/u', $valueTrim)) {
$label = $valueTrim;
} else {
$i18n = $valueTrim;
}
}
} elseif (is_array($value)) {
if (!empty($value['label'])) {
$label = (string)$value['label'];
}
if (!empty($value['i18n'])) {
$i18n = (string)$value['i18n'];
}
}
// Wenn weder Label noch i18n da ist → überspringen
if ($label === null && $i18n === null) {
continue;
}
$anchors[] = [
'href' => $href,
'label' => $label ?? '',
'i18n' => $i18n ?? '',
];
}
return $anchors;
}