v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/activity-log.php';
- require_once __DIR__ . '/includes/site-front.php';
- require_once __DIR__ . '/includes/language.php';
- require_once __DIR__ . '/includes/block-editor.php';
-
- Auth::boot();
- Auth::requireLogin();
-
- $currentUser = Auth::user();
- $db = Database::site();
-
- $type = (string) ($_GET['type'] ?? '');
- $entityId = (int) ($_GET['id'] ?? 0);
-
- $sources = [
- 'product' => ['table' => 'products', 'label' => 'product', 'fields' => ['title', 'excerpt', 'blocks', 'seo_title', 'seo_description'], 'back' => 'dashboard.php?view=product-edit&id='],
- 'page' => ['table' => 'pages', 'label' => 'page', 'fields' => ['title', 'blocks'], 'back' => 'dashboard.php?view=pages&page_id='],
- 'category' => ['table' => 'categories', 'label' => 'category', 'fields' => ['title', 'excerpt'], 'back' => 'dashboard.php?view=categories&ref='],
- ];
-
- if (!isset($sources[$type]) || $entityId <= 0) {
- http_response_code(404);
- exit('Nothing to translate.');
- }
-
- $source = $sources[$type];
- $statement = $db->prepare('SELECT * FROM ' . $source['table'] . ' WHERE id = :id LIMIT 1');
- $statement->execute(['id' => $entityId]);
- $original = $statement->fetch();
-
- if (!$original || ($type === 'product' && $original['deleted_at'] !== null)) {
- http_response_code(404);
- exit('Nothing to translate.');
- }
-
- $isEditor = Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER);
- $canTranslate = $type === 'product'
- ? ($isEditor || (int) $original['created_by'] === (int) $currentUser['id'])
- : $isEditor;
-
- if (!$canTranslate) {
- http_response_code(403);
- exit('You do not have permission to translate this item.');
- }
-
- $originalTitle = (string) ($type === 'category' ? $original['name'] : $original['title']);
- $originalExcerpt = (string) ($type === 'category' ? ($original['description'] ?? '') : ($original['excerpt'] ?? ''));
- $originalBlocks = (string) ($original['content_blocks'] ?? '');
- $originalSeoTitle = (string) ($original['seo_title'] ?? '');
- $originalSeoDescription = (string) ($original['seo_description'] ?? '');
-
- $languages = Languages::all();
- if (empty($languages)) {
- $noLanguages = true;
- $lang = '';
- } else {
- $noLanguages = false;
- $lang = strtolower(trim((string) ($_REQUEST['lang'] ?? '')));
- if (Languages::find($lang) === null) {
- $lang = $languages[0]['code'];
- }
- }
-
- $blocksAreEmpty = static function (string $json): bool {
- $decoded = json_decode($json, true);
-
- return !is_array($decoded) || empty($decoded['blocks']);
- };
-
- $selfUrl = static fn(string $code, string $extra = ''): string => 'translate.php?type=' . urlencode($type) . '&id=' . $entityId . '&lang=' . urlencode($code) . $extra;
-
- $flashMessage = null;
- $flashType = 'success';
-
- if ($noLanguages === false && $_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
- $flashMessage = 'Security check failed, please try again.';
- $flashType = 'error';
- } else {
- $action = (string) ($_POST['action'] ?? '');
- $existing = Languages::translation($type, $entityId, $lang) ?? [];
-
- if ($action === 'delete') {
- Languages::deleteTranslation($type, $entityId, $lang);
- ActivityLog::record('translation.delete', $type, $entityId, $lang);
- header('Location: ' . $selfUrl($lang, '&done=deleted'));
- exit;
- }
-
- if ($action === 'copy') {
- $merged = [
- 'title' => trim((string) ($existing['title'] ?? '')) !== '' ? (string) $existing['title'] : $originalTitle,
- 'excerpt' => trim((string) ($existing['excerpt'] ?? '')) !== '' ? (string) $existing['excerpt'] : $originalExcerpt,
- 'content_blocks' => $blocksAreEmpty((string) ($existing['content_blocks'] ?? '')) ? $originalBlocks : (string) $existing['content_blocks'],
- 'seo_title' => trim((string) ($existing['seo_title'] ?? '')) !== '' ? (string) $existing['seo_title'] : $originalSeoTitle,
- 'seo_description' => trim((string) ($existing['seo_description'] ?? '')) !== '' ? (string) $existing['seo_description'] : $originalSeoDescription,
- ];
- if (!in_array('blocks', $source['fields'], true)) {
- $merged['content_blocks'] = '';
- }
- Languages::saveTranslation($type, $entityId, $lang, $merged);
- ActivityLog::record('translation.copy', $type, $entityId, $lang);
- header('Location: ' . $selfUrl($lang, '&done=copied'));
- exit;
- }
-
- if ($action === 'save') {
- $title = mb_substr(trim((string) ($_POST['title'] ?? '')), 0, 255);
- $fields = [
- 'title' => $title,
- 'excerpt' => in_array('excerpt', $source['fields'], true) ? trim((string) ($_POST['excerpt'] ?? '')) : '',
- 'content_blocks' => in_array('blocks', $source['fields'], true) ? BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'), $type === 'page') : '',
- 'seo_title' => in_array('seo_title', $source['fields'], true) ? mb_substr(trim((string) ($_POST['seo_title'] ?? '')), 0, 255) : '',
- 'seo_description' => in_array('seo_description', $source['fields'], true) ? mb_substr(trim((string) ($_POST['seo_description'] ?? '')), 0, 500) : '',
- ];
-
- if ($title === '') {
- $flashMessage = 'The translated title is required. To remove the translation use "Remove translation".';
- $flashType = 'error';
- } else {
- Languages::saveTranslation($type, $entityId, $lang, $fields);
- ActivityLog::record('translation.save', $type, $entityId, $lang);
- header('Location: ' . $selfUrl($lang, '&done=saved'));
- exit;
- }
- }
- }
- }
-
- $done = (string) ($_GET['done'] ?? '');
- if ($flashMessage === null && $done !== '') {
- $flashMessage = ['saved' => 'Translation saved.', 'copied' => 'Missing fields copied from the original. Existing translations were not changed.', 'deleted' => 'Translation removed.'][$done] ?? null;
- }
-
- $translation = $noLanguages ? null : Languages::translation($type, $entityId, $lang);
- $currentLanguage = $noLanguages ? null : Languages::find($lang);
- $translatedCodes = [];
- foreach ($languages as $language) {
- $found = Languages::translation($type, $entityId, $language['code']);
- if ($found !== null && trim((string) $found['title']) !== '') {
- $translatedCodes[$language['code']] = true;
- }
- }
-
- $formTitle = (string) ($translation['title'] ?? '');
- $formExcerpt = (string) ($translation['excerpt'] ?? '');
- $formSeoTitle = (string) ($translation['seo_title'] ?? '');
- $formSeoDescription = (string) ($translation['seo_description'] ?? '');
- $formBlocks = $translation !== null && !$blocksAreEmpty((string) ($translation['content_blocks'] ?? '')) ? (string) $translation['content_blocks'] : '{"blocks":[]}';
- $hasBlocks = in_array('blocks', $source['fields'], true);
-
- $dashActivePage = $type === 'category' ? 'categories' : '';
- $dashView = $type === 'product' ? 'product-edit' : ($type === 'page' ? 'pages' : 'categories');
- $dashPageTitle = 'Translate';
- $dashLoadBlockEditor = $hasBlocks && !$noLanguages;
-
- require __DIR__ . '/includes/dash-header.php';
-
- $hint = static function (string $text): string {
- $text = trim($text);
- if ($text === '') {
- return '';
- }
- if (mb_strlen($text) > 220) {
- $text = mb_substr($text, 0, 219) . '…';
- }
-
- return '<p style="font-size:12px;color:var(--muted-dim);margin:4px 0 0;">Original: ' . htmlspecialchars($text) . '</p>';
- };
-
- ?>
-
- <div class="dash-header-row">
- <h1 class="dash-title"><?= Icons::icon('translate', 'icon icon-lg') ?>Translate <?= htmlspecialchars($source['label']) ?></h1>
- <a href="<?= htmlspecialchars($source['back'] . $entityId, ENT_QUOTES) ?>" class="dash-btn"><?= Icons::icon('arrow-up', 'icon icon-sm') ?>Back to original</a>
- </div>
-
- <?php if ($flashMessage !== null): ?>
- <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
- <?php endif; ?>
-
- <?php if ($noLanguages): ?>
- <div class="sidebar-box" style="max-width:640px;">
- <p style="margin:0 0 12px;">No languages have been added yet.</p>
- <?php if ($isEditor): ?>
- <a href="languages.php" class="dash-btn dash-btn-primary"><?= Icons::icon('translate', 'icon icon-sm') ?>Add a language</a>
- <?php else: ?>
- <p style="margin:0;color:var(--muted);">Ask an editor in chief to add one in Languages.</p>
- <?php endif; ?>
- </div>
- <?php else: ?>
-
- <div class="settings-tabs">
- <?php foreach ($languages as $language): ?>
- <a href="<?= htmlspecialchars($selfUrl($language['code']), ENT_QUOTES) ?>" class="<?= $language['code'] === $lang ? 'active' : '' ?>">
- <?= Icons::icon(isset($translatedCodes[$language['code']]) ? 'check' : 'translate', 'icon icon-sm') ?><?= htmlspecialchars($language['name']) ?>
- </a>
- <?php endforeach; ?>
- </div>
-
- <p style="color:var(--muted);margin:0 0 18px;">Original (<?= htmlspecialchars(Languages::defaultName()) ?>): <strong><?= htmlspecialchars($originalTitle) ?></strong>
- · Translating to <strong><?= htmlspecialchars((string) $currentLanguage['name']) ?></strong>
- <?php if ((int) $currentLanguage['is_active'] !== 1): ?>· <span class="status-pill status-draft">language inactive</span><?php endif; ?></p>
-
- <form method="post" id="article-form" class="article-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save">
- <input type="hidden" name="lang" value="<?= htmlspecialchars($lang, ENT_QUOTES) ?>">
- <input type="hidden" name="article_id" value="translation-<?= htmlspecialchars($type . '-' . $entityId . '-' . $lang, ENT_QUOTES) ?>">
- <input type="hidden" name="blocks_json" id="blocks_json_input">
-
- <div class="article-form-main" style="max-width:900px;">
- <label><?= Icons::icon('heading', 'icon icon-sm') ?>Title</label>
- <input type="text" name="title" value="<?= htmlspecialchars($formTitle) ?>" maxlength="255" required>
- <?= $hint($originalTitle) ?>
-
- <?php if (in_array('excerpt', $source['fields'], true)): ?>
- <label><?= Icons::icon('quote', 'icon icon-sm') ?><?= $type === 'category' ? 'Description' : 'Excerpt' ?></label>
- <textarea name="excerpt" rows="<?= $type === 'category' ? 3 : 2 ?>"><?= htmlspecialchars($formExcerpt) ?></textarea>
- <?= $hint($originalExcerpt) ?>
- <?php endif; ?>
-
- <?php if ($hasBlocks): ?>
- <label><?= Icons::icon('grid', 'icon icon-sm') ?>Content</label>
- <div id="block-editor-root" data-initial-blocks='<?= htmlspecialchars($formBlocks, ENT_QUOTES) ?>'></div>
- <?php if (!$blocksAreEmpty($originalBlocks)): ?>
- <details class="sidebar-box" style="margin-top:14px;">
- <summary style="cursor:pointer;font-weight:600;">Show original content</summary>
- <div class="article-content" style="margin-top:12px;"><?= BlockEditor::render($originalBlocks, false) ?></div>
- </details>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if (in_array('seo_title', $source['fields'], true)): ?>
- <label>SEO title</label>
- <input type="text" name="seo_title" value="<?= htmlspecialchars($formSeoTitle) ?>" maxlength="255">
- <?= $hint($originalSeoTitle) ?>
- <label>SEO description</label>
- <textarea name="seo_description" rows="2" maxlength="500"><?= htmlspecialchars($formSeoDescription) ?></textarea>
- <?= $hint($originalSeoDescription) ?>
- <?php endif; ?>
-
- <div class="publish-actions">
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save translation</button>
- </div>
- </div>
- </form>
-
- <div class="publish-actions" style="margin-top:14px;">
- <form method="post">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="copy">
- <input type="hidden" name="lang" value="<?= htmlspecialchars($lang, ENT_QUOTES) ?>">
- <button type="submit" class="dash-btn"><?= Icons::icon('copy', 'icon icon-sm') ?>Copy missing fields from the original</button>
- </form>
- <?php if ($translation !== null): ?>
- <form method="post" onsubmit="return confirm('Remove this translation? The original is not affected.');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="delete">
- <input type="hidden" name="lang" value="<?= htmlspecialchars($lang, ENT_QUOTES) ?>">
- <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Remove translation</button>
- </form>
- <?php endif; ?>
- </div>
- <p style="font-size:12.5px;color:var(--muted);margin:10px 0 0;">Copying only fills fields that are still empty. Anything already translated stays as it is. Fields left empty show the original text to visitors.</p>
-
- <?php endif; ?>
-
- <?php require __DIR__ . '/includes/dash-footer.php'; ?>
-