| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__).'/includes/functions.php';
require_once dirname(__DIR__).'/includes/polana_translate_client.php';
require_login();
require_admin_role();
start_session();
function polana_site_candidates(string $scope): array
{
if ($scope === 'setting') {
$all = [];
foreach (polana_site_translation_columns() as $key=>$label) {
$pl = trim(setting($key));
if ($pl !== '') $all[polana_site_translation_hash('setting',$key)] = [
'source'=>$key,'pl'=>$pl,'label'=>$label,'scope'=>'setting',
];
}
return $all;
}
$all = [];
foreach (polana_english_strings() as $pl=>$en) {
$pl = preg_replace('/\s+/u', ' ', trim((string)$pl));
if ($pl === '' || $pl === null) continue;
$all[polana_site_translation_hash('dictionary',$pl)] = [
'source'=>$pl, 'pl'=>$pl,'label'=>'Interfejs / tekst strony','scope'=>'dictionary',
];
}
// Extra phrases can be added by the editor without modifying PHP sources.
foreach (polana_site_existing() as $stored) {
if (($stored['scope'] ?? '') !== 'dictionary') continue;
$pl=(string)$stored['source_text'];
$hash=polana_site_translation_hash('dictionary',$pl);
if (!isset($all[$hash])) $all[$hash]=['source'=>$pl,'pl'=>$pl,'label'=>'Własny tekst strony','scope'=>'dictionary'];
}
return $all;
}
function polana_site_existing(): array
{
if (!polana_site_translation_ready()) return [];
$rows = db()->query('SELECT * FROM polana_site_translations')->fetchAll(PDO::FETCH_ASSOC);
$indexed = [];
foreach ($rows as $r) $indexed[$r['translation_id']] = $r;
return $indexed;
}
function polana_site_json(array $data, int $status=200): never
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($data,JSON_UNESCAPED_UNICODE|JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
$scope = (string)($_POST['scope'] ?? $_GET['scope'] ?? 'setting');
if (!in_array($scope,['setting','dictionary'],true)) $scope = 'setting';
$jobKey='polana_site_machine_'.$scope;
$err='';
$note='';
$existing = polana_site_existing();
$candidates = polana_site_candidates($scope);
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
verify_csrf();
$action = (string)($_POST['action'] ?? '');
try {
if (!polana_site_translation_ready()) {
throw new RuntimeException('Najpierw uruchom migrację bazy w panelu PL/EN.');
}
if ($action === 'begin') {
if (!polana_mt_configured()) throw new RuntimeException('Skonfiguruj HTTPS i X-TKLM-Translate-Key dla translate.tklm.pl.');
$rows=[];
foreach ($candidates as $hash=>$row) {
if (isset($existing[$hash]) && (string)$existing[$hash]['source_text'] === (string)$row['source'] && $scope==='dictionary' && trim((string)$existing[$hash]['translated_text'])!=='') continue;
if (isset($existing[$hash]) && $scope==='setting' && (string)$existing[$hash]['source_text'] === (string)$row['pl']) continue;
// Changed Polish source may be retranslated; never overwrite unchanged reviewed text.
if (strlen($row['pl']) > 10000) continue;
$rows[$hash] = $row + ['en'=>''];
}
if (!$rows) throw new RuntimeException('Wszystkie dostępne teksty mają już tłumaczenie robocze lub opublikowane. Poniżej możesz je edytować.');
$_SESSION[$jobKey] = ['created_at'=>time(),'rows'=>$rows];
redirect('/admin/translate-site.php?scope='.$scope);
}
if ($action === 'step') {
$job=$_SESSION[$jobKey] ?? null;
if (!is_array($job) || time()-(int)($job['created_at']??0)>7200) throw new RuntimeException('Sesja wygasła. Rozpocznij ponownie.');
$first=null;
foreach ($job['rows'] as $hash=>$row) {if ($row['en']==='') {$first=$hash;break;}}
if ($first !== null) {
// Translate one site field at a time; long paragraphs are split safely.
$result=polana_mt_long_text((string)$job['rows'][$first]['pl']);
$_SESSION[$jobKey]['rows'][$first]['en']=$result;
}
$pending=count(array_filter($_SESSION[$jobKey]['rows'], static fn($r)=>$r['en']===''));
polana_site_json(['ok'=>true,'remaining'=>$pending,'total'=>count($_SESSION[$jobKey]['rows'])]);
}
if ($action === 'save_generated') {
$job=$_SESSION[$jobKey]??null;
if (!is_array($job) || time()-(int)($job['created_at']??0)>7200) throw new RuntimeException('Sesja wygasła.');
if (array_filter($job['rows'],static fn($r)=>$r['en']==='')) throw new RuntimeException('Wciąż są teksty do przetłumaczenia.');
$pdo=db();$pdo->beginTransaction();
try {
$select=$pdo->prepare('SELECT source_text FROM polana_site_translations WHERE translation_id=? FOR UPDATE');
$insert=$pdo->prepare("INSERT INTO polana_site_translations (translation_id,scope,source_text,translated_text,state) VALUES (?,?,?,?,'draft')");
$update=$pdo->prepare("UPDATE polana_site_translations SET source_text=?, translated_text=?, state='draft',reviewed_at=NULL WHERE translation_id=?");
foreach ($job['rows'] as $hash=>$row) {
$source=$row['scope']==='setting'?$row['pl']:$row['source'];
$select->execute([$hash]);
$old=$select->fetchColumn();
if ($old===false) $insert->execute([$hash,$row['scope'],$source,$row['en']]);
elseif ((string)$old !== $source) $update->execute([$source,$row['en'],$hash]);
else {
$fill=$pdo->prepare("UPDATE polana_site_translations SET translated_text=?,state='draft',reviewed_at=NULL WHERE translation_id=? AND translated_text=''");
$fill->execute([$row['en'],$hash]);
}
}
$pdo->commit();
} catch (Throwable $e) {if($pdo->inTransaction())$pdo->rollBack();throw $e;}
unset($_SESSION[$jobKey]);
admin_log('site_mt_drafts','Utworzono szkice EN dla '.$scope);
flash('success','Wygenerowano robocze tłumaczenia EN. ŻADNE nie zostało opublikowane. Przejrzyj i opublikuj każde osobno.');
redirect('/admin/translate-site.php?scope='.$scope);
}
if ($action === 'cancel') {unset($_SESSION[$jobKey]);redirect('/admin/translate-site.php?scope='.$scope);}
if ($action==='add_phrase') {
if ($scope!=='dictionary') throw new RuntimeException('Nowe zwroty dodaj w zakładce Napisy interfejsu.');
$pl=trim((string)($_POST['source_pl']??''));
$pl=preg_replace('/\s+/u',' ',$pl)??'';
if ($pl==='' || strlen($pl)>1500 || str_contains($pl,'<') || str_contains($pl,'>')) {
throw new RuntimeException('Podaj polski tekst bez HTML, maksymalnie 1500 bajtów.');
}
$hash=polana_site_translation_hash('dictionary',$pl);
$insert=db()->prepare("INSERT IGNORE INTO polana_site_translations (translation_id,scope,source_text,translated_text,state) VALUES (?,'dictionary',?,'','draft')");
$insert->execute([$hash,$pl]);
flash('success','Dodano polski zwrot. Wybierz generowanie brakujących EN lub wpisz własne tłumaczenie.');
redirect('/admin/translate-site.php?scope=dictionary&q='.rawurlencode(substr($pl,0,80)));
}
if (in_array($action,['save','publish','withdraw'],true)) {
$hash=(string)($_POST['translation_id']??'');
if (!preg_match('/^[a-f0-9]{64}$/D',$hash) || !isset($candidates[$hash])) throw new RuntimeException('Nieznane tłumaczenie.');
$row=$candidates[$hash];
$english=trim((string)($_POST['english']??''));
if ($action!=='withdraw' && ($english==='' || strlen($english)>12000)) throw new RuntimeException('Tekst EN musi mieć od 1 do 12000 znaków.');
if ($action==='withdraw') {
$stmt=db()->prepare("UPDATE polana_site_translations SET state='draft',reviewed_at=NULL WHERE translation_id=? AND scope=?");
$stmt->execute([$hash,$scope]);
$note='Cofnięto do szkicu. Goście nie widzą tej wersji.';
} else {
// Publishing is only possible from an explicit review checkbox.
if ($action==='publish' && empty($_POST['verified'])) throw new RuntimeException('Zaznacz ręczne sprawdzenie tłumaczenia przed publikacją.');
$state=$action==='publish'?'published':'draft';
$stmt=db()->prepare("INSERT INTO polana_site_translations (translation_id,scope,source_text,translated_text,state,reviewed_at) VALUES (?,?,?,?,?,".($action==='publish'?'NOW()':'NULL').") ON DUPLICATE KEY UPDATE source_text=VALUES(source_text),translated_text=VALUES(translated_text),state=VALUES(state),reviewed_at=VALUES(reviewed_at)");
$stmt->execute([$hash,$scope,$scope==='setting'?$row['pl']:$row['source'],$english,$state]);
$note=$action==='publish'?'Zatwierdzono i opublikowano EN.':'Zapisano roboczą wersję EN. Bez publikacji.';
}
admin_log('site_mt_'.$action, $scope.' '.$hash);
$existing=polana_site_existing();
}
} catch(Throwable $e) {
if ($action==='step') polana_site_json(['ok'=>false,'message'=>$e->getMessage()],502);
$err=$e->getMessage();
}
}
$job=$_SESSION[$jobKey]??null;
if (is_array($job) && time()-(int)($job['created_at']??0)>7200) {unset($_SESSION[$jobKey]);$job=null;}
$remaining=is_array($job)?count(array_filter($job['rows'],static fn($r)=>$r['en']==='')):0;
$filter=trim((string)($_GET['q']??''));
$filter=substr($filter,0,100);
$filtered=array_filter($candidates,static function($row)use($filter){return $filter===''||stripos($row['pl'].' '.$row['label'],$filter)!==false;});
$page=max(1,(int)($_GET['page']??1));
$perPage=15;
$pages=max(1,(int)ceil(count($filtered)/$perPage));$page=min($page,$pages);
$view=array_slice($filtered,($page-1)*$perPage,$perPage,true);
render_admin_header('Tłumaczenie WWW PL → EN');
?>
<div class="admin-title-row"><div><h1>🇬🇧 Tłumaczenie strony przez Docker</h1><p>translate.tklm.pl · wyłącznie szkice do czasu ręcznego sprawdzenia</p></div><a class="btn btn-light" href="/admin/settings.php">Ustawienia</a></div>
<section class="form-card">
<p style="padding:14px;border:1px solid #dfa31a;border-radius:10px"><strong>⚠️ Tłumaczenie maszynowe — sprawdź przed publikacją.</strong> Zweryfikuj nazwy własne, adresy, informacje dla klientów i zgodność SEO. Samo wygenerowanie tłumaczenia niczego nie publikuje.</p>
<p><strong>Konfiguracja API:</strong> <?= polana_mt_configured()?'URL i klucz ustawione (połączenie testowane przy użyciu)':'brak – skonfiguruj includes/polana_translate_config.local.php' ?>.</p>
<?php if ($err): ?><div class="alert alert-error"><?= e($err) ?></div><?php endif; ?>
<?php if ($note): ?><div class="alert alert-success"><?= e($note) ?></div><?php endif; ?>
<?php if (!polana_site_translation_ready()): ?><p><a href="/admin/polana-bilingual-install.php">Uruchom migrację bazy tłumaczeń →</a></p><?php endif; ?>
<p><a class="btn <?= $scope==='setting'?'btn-green':'btn-light' ?>" href="?scope=setting">Treści strony i SEO</a> <a class="btn <?= $scope==='dictionary'?'btn-green':'btn-light' ?>" href="?scope=dictionary">Napisy interfejsu (<?= count(polana_site_candidates('dictionary')) ?>)</a></p>
<?php if ($job): ?>
<div id="polana-site-progress" role="status" aria-live="polite">Do przetłumaczenia: <?= $remaining ?> / <?= count($job['rows']) ?></div>
<?php if ($remaining): ?><button id="polana-site-run" class="btn btn-primary" type="button">Rozpocznij / wznów tłumaczenie</button>
<?php else: ?><form method="post"><?= csrf_field() ?><input type="hidden" name="scope" value="<?= e($scope) ?>"><input type="hidden" name="action" value="save_generated"><button class="btn btn-green" type="submit">Zapisz wszystkie jako SZKICE (nie publikuj)</button></form><?php endif; ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="scope" value="<?= e($scope) ?>"><input type="hidden" name="action" value="cancel"><button class="btn btn-light" type="submit">Anuluj generowanie</button></form>
<?php else: ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="scope" value="<?= e($scope) ?>"><input type="hidden" name="action" value="begin"><button class="btn btn-primary" type="submit" <?= !polana_site_translation_ready()?'disabled':'' ?>>Wygeneruj brakujące tłumaczenia PL → EN</button></form>
<?php endif; ?>
<?php if($scope==='dictionary'): ?>
<form method="post" style="margin-top:20px"><?= csrf_field() ?><input type="hidden" name="scope" value="dictionary"><input type="hidden" name="action" value="add_phrase">
<label><strong>Dodaj brakujący polski tekst strony</strong><textarea name="source_pl" rows="2" maxlength="1500" placeholder="Wklej dokładny tekst wyświetlany na stronie" style="width:100%"></textarea></label>
<button type="submit" class="btn btn-light">Dodaj do szkiców EN</button>
<p class="muted">Dotyczy tekstów renderowanych jako zwykłe elementy HTML. Nie dotyczy treści generowanych przez JavaScript ani plików graficznych.</p>
</form>
<?php endif; ?>
<p class="muted">Dotychczasowe teksty EN pozostają dostępne. Maszynowe tłumaczenia zapisujemy tylko w nowych szkicach, nie nadpisujemy ręcznych zmian.</p>
</section>
<?php if (!$job): ?>
<section class="form-card"><h2>Tłumaczenia — edycja i publikacja</h2>
<form method="get"><input type="hidden" name="scope" value="<?= e($scope) ?>"><input name="q" value="<?= e($filter) ?>" placeholder="Szukaj po polskim tekście"><button type="submit" class="btn btn-light">Szukaj</button></form>
<p><?= count($filtered) ?> tekstów · strona <?= $page ?> z <?= $pages ?></p>
<?php foreach ($view as $hash=>$item): $stored=$existing[$hash]??null; $defaultEnglish=$scope==='dictionary'?(polana_english_strings()[$item['source']]??''):setting($item['source'].'_en'); ?>
<form method="post" style="margin:14px 0;padding:14px;border:1px solid #ddd;border-radius:12px">
<?= csrf_field() ?><input type="hidden" name="scope" value="<?= e($scope) ?>"><input type="hidden" name="translation_id" value="<?= e($hash) ?>">
<strong><?= e($item['label']) ?></strong> <?php if ($scope==='setting' && $stored && (string)$stored['source_text'] !== $item['pl']): ?><small style="color:#9b2424">⚠️ Polski tekst zmieniono od poprzedniego tłumaczenia — wymaga ponownego zatwierdzenia.</small><?php endif; ?>
<div style="white-space:pre-wrap;margin:6px 0 10px"><?= e($item['pl']) ?></div>
<label>Angielska wersja <small>· <?= e($stored['state']??'bez szkicu') ?></small>
<textarea name="english" rows="3" maxlength="12000" style="width:100%;display:block"><?= e((string)($stored['translated_text']??$defaultEnglish)) ?></textarea></label>
<label style="display:block;margin:10px 0"><input type="checkbox" name="verified" value="1"> <strong>Sprawdziłem tłumaczenie maszynowe i zatwierdzam publikację.</strong></label>
<button type="submit" name="action" value="save" class="btn btn-light">Zapisz szkic</button> <button type="submit" name="action" value="publish" class="btn btn-green">Zatwierdź i opublikuj EN</button>
<?php if (($stored['state']??'')==='published'): ?><button type="submit" name="action" value="withdraw" class="btn btn-light">Wycofaj EN do szkicu</button><?php endif; ?>
</form>
<?php endforeach; ?>
<?php if ($pages>1): ?><div><a href="?scope=<?= e($scope) ?>&q=<?= urlencode($filter) ?>&page=<?= max(1,$page-1) ?>">← Poprzednia</a> · <?= $page ?>/<?= $pages ?> · <a href="?scope=<?= e($scope) ?>&q=<?= urlencode($filter) ?>&page=<?= min($pages,$page+1) ?>">Następna →</a></div><?php endif; ?>
</section>
<?php endif; ?>
<?php if ($job && $remaining): ?>
<script>
(()=>{const b=document.getElementById('polana-site-run'),p=document.getElementById('polana-site-progress');if(!b||!p)return;b.onclick=async()=>{b.disabled=true;try{for(;;){let form=new URLSearchParams({csrf_token:<?= json_encode(csrf_token()) ?>,scope:<?= json_encode($scope) ?>,action:'step'});let r=await fetch('/admin/translate-site.php?scope=<?= e($scope) ?>',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:form});let x=await r.json();if(!r.ok||!x.ok)throw Error(x.message||('HTTP '+r.status));p.textContent='Przetłumaczono '+(x.total-x.remaining)+' z '+x.total+' tekstów';if(x.remaining===0){location.reload();break;}}}catch(e){p.textContent='Błąd: '+e.message+' — możesz wznowić.';b.disabled=false;}};})();
</script>
<?php endif; ?>
<?php render_admin_footer(); ?>