149 lines
6.5 KiB
PHP
149 lines
6.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Sprachwahl der Verwaltung (Deutsch ist die Quellsprache, English kommt aus lang/en.json).
|
|
*
|
|
* Übersetzt wird die fertige Seite, so wie bei DeskHub selbst: Textstücke zwischen den HTML-Tags und die Attribute placeholder, title,
|
|
* alt, aria-label und data-confirm. Ein Schlüssel ist der deutsche Text (Leerraum zählt nicht); "{}" steht für einen wechselnden Teil
|
|
* und "{#}" für eine Zahl; in der Übersetzung stehen "{1}", "{2}" für die gefundenen Teile (oder "{}" / "{#}" der Reihe nach).
|
|
* Was nicht übersetzt ist, bleibt deutsch. Inhalte in <code>, <pre>, <script> und <style> bleiben unverändert.
|
|
*
|
|
* Die Sprache steht in einem Cookie (site_lang); ?lang=en oder ?lang=de schaltet um.
|
|
*/
|
|
|
|
const SITE_LANGS = ['de' => 'Deutsch', 'en' => 'English'];
|
|
|
|
/** Gewählte Sprache: ?lang= (wird im Cookie gemerkt), sonst das Cookie, sonst Deutsch. */
|
|
function site_lang(): string
|
|
{
|
|
static $lang = null;
|
|
if ($lang !== null) return $lang;
|
|
$lang = 'de';
|
|
$asked = (string) ($_GET['lang'] ?? '');
|
|
if (isset(SITE_LANGS[$asked])) {
|
|
$lang = $asked;
|
|
if (!headers_sent()) {
|
|
setcookie('site_lang', $asked, ['expires' => time() + 365 * 86400, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Strict']);
|
|
}
|
|
} elseif (isset(SITE_LANGS[(string) ($_COOKIE['site_lang'] ?? '')])) {
|
|
$lang = (string) $_COOKIE['site_lang'];
|
|
}
|
|
return $lang;
|
|
}
|
|
|
|
/** @return array{exact:array<string,string>,patterns:list<array{0:string,1:string}>} */
|
|
function site_i18n_dictionary(string $lang): array
|
|
{
|
|
static $cache = [];
|
|
if (isset($cache[$lang])) return $cache[$lang];
|
|
$exact = [];
|
|
$patterns = [];
|
|
$file = dirname(__DIR__) . '/lang/' . $lang . '.json';
|
|
$data = $lang !== 'de' && preg_match('/^[a-z]{2}$/', $lang) && is_file($file) ? json_decode((string) file_get_contents($file), true) : null;
|
|
if (is_array($data)) {
|
|
foreach ($data as $de => $tr) {
|
|
if (!is_string($de) || !is_string($tr)) continue;
|
|
$key = site_i18n_norm($de);
|
|
if (strpos($key, '{}') === false && strpos($key, '{#}') === false) {
|
|
$exact[$key] = $tr;
|
|
continue;
|
|
}
|
|
$quoted = preg_quote($key, '#');
|
|
$regex = '#^' . str_replace(['\{\}', '\{\#\}'], ['(.+?)', '(\d+)'], $quoted) . '$#su';
|
|
$patterns[] = [$regex, $tr];
|
|
}
|
|
// Längere (genauere) Muster zuerst
|
|
usort($patterns, fn($a, $b) => strlen($b[0]) <=> strlen($a[0]));
|
|
}
|
|
return $cache[$lang] = ['exact' => $exact, 'patterns' => $patterns];
|
|
}
|
|
|
|
function site_i18n_norm(string $s): string
|
|
{
|
|
return trim((string) preg_replace('/\s+/u', ' ', $s));
|
|
}
|
|
|
|
/** Übersetzt einen (entkodierten) Text oder gibt null zurück, wenn es keine Übersetzung gibt. */
|
|
function site_i18n_lookup(string $norm, array $dict, int $depth = 0): ?string
|
|
{
|
|
if ($norm === '') return null;
|
|
if (isset($dict['exact'][$norm])) return $dict['exact'][$norm];
|
|
foreach ($dict['patterns'] as [$regex, $tr]) {
|
|
if (!preg_match($regex, $norm, $m)) continue;
|
|
array_shift($m);
|
|
// Gefundene Teile selbst übersetzen, wenn möglich (zum Beispiel der Seitenname im Titel)
|
|
if ($depth < 2) foreach ($m as $i => $part) $m[$i] = site_i18n_lookup(site_i18n_norm($part), $dict, $depth + 1) ?? $part;
|
|
if (preg_match('/\{\d+\}/', $tr)) {
|
|
return (string) preg_replace_callback('/\{(\d+)\}/', fn($x) => $m[(int) $x[1] - 1] ?? '', $tr);
|
|
}
|
|
$i = 0;
|
|
return (string) preg_replace_callback('/\{#?\}/', function () use (&$i, $m) { return $m[$i++] ?? ''; }, $tr);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Übersetzt eine fertige HTML-Seite. */
|
|
function site_translate_html(string $html, string $lang): string
|
|
{
|
|
$dict = site_i18n_dictionary($lang);
|
|
if (!$dict['exact'] && !$dict['patterns']) return $html;
|
|
$h = static fn(string $s): string => htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
$parts = preg_split('/(<!--.*?-->|<[^<>]*>)/su', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
$skip = 0;
|
|
$out = '';
|
|
foreach ($parts as $i => $part) {
|
|
if ($part === '') continue;
|
|
if ($i % 2 === 1) {
|
|
if (preg_match('/^<\s*(\/?)\s*(script|style|code|pre)\b/i', $part, $m)) {
|
|
$skip = max(0, $skip + ($m[1] === '/' ? -1 : (substr($part, -2) === '/>' ? 0 : 1)));
|
|
}
|
|
if ($part[1] !== '!' && $part[1] !== '/') {
|
|
$part = preg_replace_callback('/\b(placeholder|title|alt|aria-label|data-confirm)="([^"]*)"/i', function ($m) use ($dict, $h) {
|
|
$t = site_i18n_lookup(site_i18n_norm(html_entity_decode($m[2], ENT_QUOTES | ENT_HTML5, 'UTF-8')), $dict);
|
|
return $t === null ? $m[0] : $m[1] . '="' . $h($t) . '"';
|
|
}, $part) ?? $part;
|
|
}
|
|
$out .= $part;
|
|
continue;
|
|
}
|
|
if ($skip > 0) { $out .= $part; continue; }
|
|
$decoded = html_entity_decode($part, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$norm = site_i18n_norm($decoded);
|
|
$t = site_i18n_lookup($norm, $dict);
|
|
if ($t === null) { $out .= $part; continue; }
|
|
preg_match('/^\s*/u', $part, $lead);
|
|
preg_match('/\s*$/u', $part, $trail);
|
|
$out .= ($lead[0] ?? '') . htmlspecialchars($t, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . ($trail[0] ?? '');
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/** Startet die Übersetzung der folgenden Ausgabe (nur bei einer anderen Sprache als Deutsch). */
|
|
function site_i18n_start(): void
|
|
{
|
|
static $started = false;
|
|
if ($started) return;
|
|
$lang = site_lang();
|
|
if ($lang === 'de') return;
|
|
$started = true;
|
|
ob_start(fn(string $buffer) => site_translate_html($buffer, $lang));
|
|
}
|
|
|
|
/** Umschalter für den Seitenkopf: die jeweils andere Sprache als Link (behält die übrigen Angaben der Adresse). */
|
|
function site_lang_switcher(): string
|
|
{
|
|
$current = site_lang();
|
|
$query = $_GET;
|
|
$out = '';
|
|
foreach (SITE_LANGS as $code => $name) {
|
|
if ($code === $current) {
|
|
$out .= '<span class="lang on" aria-current="true">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</span>';
|
|
continue;
|
|
}
|
|
$query['lang'] = $code;
|
|
$out .= '<a class="lang" href="?' . htmlspecialchars(http_build_query($query), ENT_QUOTES, 'UTF-8') . '" hreflang="' . $code . '">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</a>';
|
|
}
|
|
return '<span class="langs">' . $out . '</span>';
|
|
}
|