Files
racket-chatgpt-bootstrap/private/languagestore.php
T
www-data 475765e31f Move rendering into private templates
Add an explicit template renderer with HTML views and partials for the app, bootstrap, package, and catalog pages. Move shared reporting setup into config/reporting.php and relocate stylesheet assets under css/.
2026-05-26 12:50:26 +02:00

243 lines
6.5 KiB
PHP

<?php
/*
* languagestore.php
*
* Small SQLite-backed translation module.
*
* Database:
* data/racket-sandbox.sqlite
*/
class LanguageStoreException extends Exception
{
}
class LanguageStore
{
private $db;
private $fallbackLanguage;
public function __construct($dbFile, $fallbackLanguage = 'en')
{
$dir = dirname($dbFile);
if (!is_dir($dir)) {
if (!mkdir($dir, 0700, true)) {
throw new LanguageStoreException('Could not create database directory: ' . $dir);
}
}
$this->fallbackLanguage = $this->safeLanguage($fallbackLanguage);
$this->db = new PDO('sqlite:' . $dbFile);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->db->exec('PRAGMA journal_mode = WAL');
$this->db->exec('PRAGMA busy_timeout = 5000');
$this->init();
}
private function init()
{
$this->db->exec(
'CREATE TABLE IF NOT EXISTS app_translations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
translation_key TEXT NOT NULL,
language TEXT NOT NULL,
text TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(translation_key, language)
)'
);
$this->db->exec(
'CREATE INDEX IF NOT EXISTS idx_app_translations_language
ON app_translations(language, translation_key)'
);
}
public function translate($key, $language, $fallback = null)
{
$key = $this->safeKey($key);
$language = $this->safeLanguage($language);
$text = $this->findText($key, $language);
if ($text !== null) {
return $text;
}
if ($language !== $this->fallbackLanguage) {
$text = $this->findText($key, $this->fallbackLanguage);
if ($text !== null) {
return $text;
}
}
return $fallback !== null ? (string)$fallback : $key;
}
public function translateFormat($key, $language, $values = array(), $fallback = null)
{
return self::formatText($this->translate($key, $language, $fallback), $values);
}
public static function formatText($text, $values, $maxDepth = 8)
{
if (!is_array($values) || count($values) === 0) {
return (string)$text;
}
$text = (string)$text;
for ($depth = 0; $depth < $maxDepth; $depth++) {
$changed = false;
$next = preg_replace_callback('/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/', function ($match) use ($values, &$changed) {
$name = $match[1];
if (!array_key_exists($name, $values)) {
return $match[0];
}
$changed = true;
return (string)$values[$name];
}, $text);
$text = $next;
if (!$changed) {
break;
}
}
return $text;
}
public function setTranslation($key, $language, $text)
{
$key = $this->safeKey($key);
$language = $this->safeLanguage($language);
$text = (string)$text;
$now = time();
$stmt = $this->db->prepare(
'INSERT INTO app_translations
(translation_key, language, text, created_at, updated_at)
VALUES
(:translation_key, :language, :text, :created_at, :updated_at)
ON CONFLICT(translation_key, language)
DO UPDATE SET text = excluded.text, updated_at = excluded.updated_at'
);
$stmt->execute(array(
':translation_key' => $key,
':language' => $language,
':text' => $text,
':created_at' => $now,
':updated_at' => $now,
));
}
public function seedDefaults($translations)
{
foreach ($translations as $key => $byLanguage) {
foreach ($byLanguage as $language => $text) {
if (!$this->hasTranslation($key, $language)) {
$this->setTranslation($key, $language, $text);
}
}
}
}
public function listTranslations($language = null)
{
if ($language !== null && $language !== '') {
$stmt = $this->db->prepare(
'SELECT translation_key, language, text, created_at, updated_at
FROM app_translations
WHERE language = :language
ORDER BY translation_key'
);
$stmt->execute(array(':language' => $this->safeLanguage($language)));
} else {
$stmt = $this->db->query(
'SELECT translation_key, language, text, created_at, updated_at
FROM app_translations
ORDER BY translation_key, language'
);
}
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function supportedLanguages()
{
return array('en', 'nl');
}
public function languageLabel($language)
{
if ($language === 'nl') {
return 'Nederlands';
}
if ($language === 'en') {
return 'English';
}
return $language;
}
private function findText($key, $language)
{
$stmt = $this->db->prepare(
'SELECT text
FROM app_translations
WHERE translation_key = :translation_key
AND language = :language'
);
$stmt->execute(array(
':translation_key' => $key,
':language' => $language,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? (string)$row['text'] : null;
}
private function hasTranslation($key, $language)
{
return $this->findText($this->safeKey($key), $this->safeLanguage($language)) !== null;
}
private function safeKey($key)
{
$key = trim((string)$key);
if ($key === '') {
throw new LanguageStoreException('Translation key is required.');
}
if (strlen($key) > 160) {
throw new LanguageStoreException('Translation key is too long.');
}
return $key;
}
private function safeLanguage($language)
{
$language = strtolower(trim((string)$language));
if (!preg_match('/^[a-z][a-z0-9_-]{1,15}$/', $language)) {
throw new LanguageStoreException('Invalid language code.');
}
return $language;
}
}