398 lines
10 KiB
PHP
Executable File
398 lines
10 KiB
PHP
Executable File
<?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];
|
|
}
|
|
|
|
/**
|
|
* Aus einem Label einen stabilen i18n-Key für Nav-Anker bauen.
|
|
* Beispiel: "So funktioniert USBCheck!" -> "nav_so_funktioniert_usbcheck"
|
|
*/
|
|
function app_i18n_make_anchor_key(string $label): string
|
|
{
|
|
// HTML-Entities entfernen (z. B. &)
|
|
$decoded = html_entity_decode($label, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
|
// Kleinbuchstaben
|
|
$decoded = mb_strtolower($decoded, 'UTF-8');
|
|
|
|
// Alles, was kein a-z oder 0-9 ist, durch Unterstrich ersetzen
|
|
$key = preg_replace('/[^a-z0-9]+/u', '_', $decoded);
|
|
|
|
// Mehrfache Unterstriche trimmen
|
|
$key = trim($key, '_');
|
|
|
|
if ($key === '') {
|
|
$key = 'item';
|
|
}
|
|
|
|
// Prefix, damit klar ist, dass es Navigationskeys sind
|
|
return 'nav_' . $key;
|
|
}
|
|
|
|
/**
|
|
* Nav-Anker für eine Seite aus der Sprachdatei holen.
|
|
*
|
|
* Haupt-Variante im JSON:
|
|
*
|
|
* "pages": {
|
|
* "landing": {
|
|
* "anchors": {
|
|
* "how": "So funktioniert USBCheck",
|
|
* "problem": "Warum gefälschte USB-Sticks gefährlich sind",
|
|
* "features": "Funktionen",
|
|
* "security": "Sicherheit",
|
|
* "faq": "FAQ"
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* Optional explizit:
|
|
* "anchors": {
|
|
* "how": { "label": "So funktioniert USBCheck", "i18n": "nav_how" },
|
|
* "faq": { "i18n": "nav_faq" }
|
|
* }
|
|
*
|
|
* Rückgabe-Format:
|
|
* [
|
|
* [ 'href' => '#how', 'label' => 'So funktioniert USBCheck', 'i18n' => 'nav_so_funktioniert_usbcheck' ],
|
|
* [ 'href' => '#faq', 'label' => '', 'i18n' => 'nav_faq' ],
|
|
* ]
|
|
*/
|
|
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 = '';
|
|
$i18n = '';
|
|
|
|
if (is_string($value)) {
|
|
// String IMMER als Label übernehmen
|
|
$labelTrim = trim($value);
|
|
if ($labelTrim === '') {
|
|
continue;
|
|
}
|
|
|
|
$label = $labelTrim;
|
|
// i18n-Key automatisch aus dem Label ableiten
|
|
$i18n = app_i18n_make_anchor_key($labelTrim);
|
|
|
|
} elseif (is_array($value)) {
|
|
// Explizite Variante:
|
|
// "how": { "label": "...", "i18n": "nav_how" }
|
|
if (!empty($value['label'])) {
|
|
$label = trim((string)$value['label']);
|
|
}
|
|
if (!empty($value['i18n'])) {
|
|
$i18n = trim((string)$value['i18n']);
|
|
}
|
|
|
|
if ($label === '' && $i18n === '') {
|
|
continue;
|
|
}
|
|
|
|
// Wenn Label gesetzt, aber kein i18n: automatisch generieren
|
|
if ($label !== '' && $i18n === '') {
|
|
$i18n = app_i18n_make_anchor_key($label);
|
|
}
|
|
} else {
|
|
// Weder String noch Array → ignorieren
|
|
continue;
|
|
}
|
|
|
|
$anchors[] = [
|
|
'href' => $href,
|
|
'label' => $label,
|
|
'i18n' => $i18n,
|
|
];
|
|
}
|
|
|
|
return $anchors;
|
|
}
|