added translation tab

This commit is contained in:
2026-05-22 16:20:00 +02:00
parent c86768e391
commit 0291b8be7a
10 changed files with 1227 additions and 246 deletions

View File

@ -202,6 +202,166 @@ function rbac_client_filter(array $tokenRecord, string $clientAlias = 'client'):
}
}
// --- Translations: German (de) is the canonical source language ---
define('QDB_SOURCE_LANGUAGE', 'de');
function qdb_ensure_source_language(PDO $pdo): void {
$pdo->prepare("INSERT INTO language (languageCode, name) VALUES (:lc, :n)
ON CONFLICT(languageCode) DO NOTHING")
->execute([':lc' => QDB_SOURCE_LANGUAGE, ':n' => 'German']);
}
function qdb_upsert_source_translation(PDO $pdo, string $type, string $id, string $text): void {
qdb_ensure_source_language($pdo);
$lang = QDB_SOURCE_LANGUAGE;
if ($type === 'question') {
$pdo->prepare("INSERT INTO question_translation (questionID, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(questionID, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
} elseif ($type === 'answer_option') {
$pdo->prepare("INSERT INTO answer_option_translation (answerOptionID, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(answerOptionID, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
} elseif ($type === 'string') {
$pdo->prepare("INSERT INTO string_translation (stringKey, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(stringKey, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
}
}
function qdb_require_non_empty_german(string $text, string $label = 'German text'): void {
if ($text === '') {
json_error('MISSING_FIELDS', "$label is required", 400);
}
}
/**
* Build translation entry lists for one questionnaire.
* Returns ['stringEntries' => [...], 'contentEntries' => [...]] (questions + options interleaved).
*/
function qdb_translation_entry_lists(PDO $pdo, string $qnID): array {
$qStmt = $pdo->prepare("
SELECT q.questionID, q.defaultText, q.configJson, q.orderIndex
FROM question q WHERE q.questionnaireID = :id ORDER BY q.orderIndex
");
$qStmt->execute([':id' => $qnID]);
$dbQuestions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
$stringKeys = [];
$stringEntries = [];
$contentEntries = [];
foreach ($dbQuestions as $dbQ) {
$qOrder = (int)($dbQ['orderIndex'] ?? 0);
$contentEntries[] = [
'key' => $dbQ['defaultText'],
'type' => 'question',
'entityId' => $dbQ['questionID'],
'sortOrder' => $qOrder * 1000,
];
$aoStmt = $pdo->prepare("
SELECT answerOptionID, defaultText, orderIndex
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
");
$aoStmt->execute([':qid' => $dbQ['questionID']]);
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
$contentEntries[] = [
'key' => $ao['defaultText'],
'type' => 'answer_option',
'entityId' => $ao['answerOptionID'],
'sortOrder' => $qOrder * 1000 + 10 + (int)($ao['orderIndex'] ?? 0),
];
}
$config = json_decode($dbQ['configJson'] ?: '{}', true) ?: [];
foreach (['textKey', 'textKey1', 'textKey2', 'hint', 'hint1', 'hint2'] as $field) {
if (!empty($config[$field])) {
$stringKeys[$config[$field]] = true;
}
}
if (!empty($config['symptoms']) && is_array($config['symptoms'])) {
foreach ($config['symptoms'] as $s) {
$stringKeys[$s] = true;
}
}
}
$stringOrder = 0;
$skList = array_keys($stringKeys);
sort($skList, SORT_STRING);
foreach ($skList as $sk) {
$stringEntries[] = [
'key' => $sk,
'type' => 'string',
'entityId' => $sk,
'sortOrder' => $stringOrder++,
];
}
return ['stringEntries' => $stringEntries, 'contentEntries' => $contentEntries];
}
/** Attach translation texts and germanText to entry rows. */
function qdb_attach_translation_texts(array &$entries, array $translations): void {
foreach ($entries as &$e) {
$tr = $translations[$e['entityId']] ?? [];
$e['translations'] = (object)$tr;
$de = trim($tr[QDB_SOURCE_LANGUAGE] ?? '');
$e['germanText'] = $de !== '' ? $de : $e['key'];
}
unset($e);
}
/** Load all translation rows for a flat entry list. */
function qdb_load_translations_for_entries(PDO $pdo, array $entries): array {
$translations = [];
$qIds = array_values(array_filter(array_map(
fn($e) => $e['type'] === 'question' ? $e['entityId'] : null,
$entries
)));
if ($qIds) {
$ph = implode(',', array_fill(0, count($qIds), '?'));
$stmt = $pdo->prepare("SELECT questionID AS entityId, languageCode, text FROM question_translation WHERE questionID IN ($ph)");
$stmt->execute($qIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
$aoIds = array_values(array_filter(array_map(
fn($e) => $e['type'] === 'answer_option' ? $e['entityId'] : null,
$entries
)));
if ($aoIds) {
$ph = implode(',', array_fill(0, count($aoIds), '?'));
$stmt = $pdo->prepare("SELECT answerOptionID AS entityId, languageCode, text FROM answer_option_translation WHERE answerOptionID IN ($ph)");
$stmt->execute($aoIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
$sKeys = array_values(array_filter(array_map(
fn($e) => $e['type'] === 'string' ? $e['entityId'] : null,
$entries
)));
if ($sKeys) {
$ph = implode(',', array_fill(0, count($sKeys), '?'));
$stmt = $pdo->prepare("SELECT stringKey AS entityId, languageCode, text FROM string_translation WHERE stringKey IN ($ph)");
$stmt->execute($sKeys);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
return $translations;
}
// --- AES-256-CBC: IV(16) + CIPHERTEXT ---
function aes256_cbc_encrypt_bytes(string $plain, string $key): string {
$key = str_pad(substr($key, 0, 32), 32, "\0");