i18n all /dashboard/ corpus pages for en/no/uk/pl

- Add require_once bootstrap.php to all 6 dashboard page files so
  dbnToolsT() is available before layout_dashboard.php is included
- Add dash_upload_category_lbl key to no/uk/pl sections of i18n.php
  (was only in English); Kategori/Категорія/Kategoria
- Fix broken ternary on upload.php Category label — replace with
  dbnToolsT('dash_upload_category_lbl', $uiLang)
- layout_dashboard.php outputs window.DBN_I18N with all js_* keys
  so dashboard JS reads locale-aware strings from PHP translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:10:57 +02:00
parent 90117fa9de
commit a9e64b65ce
8 changed files with 886 additions and 197 deletions
+50 -28
View File
@@ -1,31 +1,32 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'documents';
$dashboardTitle = 'Dokumenter';
$dashboardLead = 'Alle dokumenter i din private korpus. Klikk for å åpne, eller velg flere for bulk-handlinger.';
$dashboardTitle = dbnToolsT('dash_title_docs', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_docs', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<section class="dash-card">
<div class="dash-filters">
<input type="search" id="docFilterQ" placeholder="Søk i titler eller tagger…" autocomplete="off">
<input type="search" id="docFilterQ" placeholder="<?= htmlspecialchars(dbnToolsT('dash_filter_q_ph', $uiLang)) ?>" autocomplete="off">
<select id="docFilterStatus">
<option value="">Alle statuser</option>
<option value="ready">Klar</option>
<option value="pending">Venter</option>
<option value="processing">Behandler</option>
<option value="error">Feil</option>
<option value=""><?= htmlspecialchars(dbnToolsT('dash_filter_all_status', $uiLang)) ?></option>
<option value="ready" id="optReady"></option>
<option value="pending" id="optPending"></option>
<option value="processing" id="optProcessing"></option>
<option value="error" id="optError"></option>
</select>
<button id="docBulkDelete" class="dash-btn dash-btn--danger" disabled>Slett valgte</button>
<a href="/dashboard/upload.php" class="dash-btn dash-btn--primary" style="margin-left: auto;">+ Last opp</a>
<button id="docBulkDelete" class="dash-btn dash-btn--danger" disabled><?= htmlspecialchars(dbnToolsT('dash_delete_selected', $uiLang)) ?></button>
<a href="/dashboard/upload.php" class="dash-btn dash-btn--primary" style="margin-left: auto;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_short', $uiLang)) ?></a>
</div>
<div id="docListWrap"><p class="dash-loading">Laster dokumenter…</p></div>
<div id="docListWrap"><p class="dash-loading"></p></div>
<div class="dash-pager" id="docPager" hidden>
<span id="docPagerLabel"></span>
<div class="dash-pager__actions">
<button class="dash-btn" id="docPagerPrev">← Forrige</button>
<button class="dash-btn" id="docPagerNext">Neste →</button>
<button class="dash-btn" id="docPagerPrev"><?= htmlspecialchars(dbnToolsT('dash_prev', $uiLang)) ?></button>
<button class="dash-btn" id="docPagerNext"><?= htmlspecialchars(dbnToolsT('dash_next', $uiLang)) ?></button>
</div>
</div>
</section>
@@ -33,9 +34,20 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const loc = I18N.locale || 'en-GB';
const PAGE = 25;
const optReady = document.getElementById('optReady');
const optPend = document.getElementById('optPending');
const optProc = document.getElementById('optProcessing');
const optErr = document.getElementById('optError');
if (optReady) optReady.textContent = I18N.status_ready || 'Ready';
if (optPend) optPend.textContent = I18N.status_pending || 'Pending';
if (optProc) optProc.textContent = I18N.status_processing || 'Processing';
if (optErr) optErr.textContent = I18N.status_error || 'Error';
const state = { offset: 0, total: 0, selected: new Set(), q: '', status: '' };
const $wrap = document.getElementById('docListWrap');
@@ -50,13 +62,16 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString('nb-NO', { day:'numeric', month:'short', year:'numeric' }); }
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString(loc, { day:'numeric', month:'short', year:'numeric' }); }
catch (_) { return s; }
}
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString('nb-NO'); }
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString(loc); }
function statusPill(status) {
const cls = { ready:'dash-status--ready', pending:'dash-status--pending', processing:'dash-status--processing', error:'dash-status--error' }[status] || 'dash-status--pending';
const lbl = { ready:'Klar', pending:'Venter', processing:'Behandler', error:'Feil' }[status] || status;
const lbl = {
ready: I18N.status_ready || 'Ready', pending: I18N.status_pending || 'Pending',
processing: I18N.status_processing || 'Processing', error: I18N.status_error || 'Error',
}[status] || status;
return '<span class="dash-status ' + cls + '">' + lbl + '</span>';
}
@@ -65,7 +80,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
if (state.q) qs.set('q', state.q);
if (state.status) qs.set('status', state.status);
$wrap.innerHTML = '<p class="dash-loading">Laster dokumenter…</p>';
$wrap.innerHTML = '<p class="dash-loading">' + (I18N.loading_docs || 'Loading documents…') + '</p>';
fetch(api + '/documents.php?' + qs, { credentials:'same-origin' })
.then(r => r.json())
.then(data => {
@@ -81,8 +96,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function render(docs) {
if (!docs.length) {
$wrap.innerHTML = '<div class="dash-empty"><span class="dash-empty__icon">📭</span>'
+ (state.q || state.status ? 'Ingen treff for valgt filter.'
: 'Ingen dokumenter ennå. <a href="/dashboard/upload.php">Last opp ditt første</a>.')
+ (state.q || state.status
? (I18N.empty_filter || 'No results for selected filter.')
: (I18N.empty_docs || 'No documents yet.') + ' <a href="/dashboard/upload.php">' + (I18N.empty_docs_link || 'Upload your first') + '</a>.')
+ '</div>';
$pager.hidden = true;
return;
@@ -93,8 +109,11 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
table.innerHTML =
'<thead><tr>'
+ '<th style="width:36px;"><input type="checkbox" id="docSelectAll"></th>'
+ '<th>Tittel</th><th>Kategori</th><th>Status</th>'
+ '<th>Passasjer</th><th>Lagt til</th>'
+ '<th>' + (I18N.th_title || 'Title') + '</th>'
+ '<th>' + (I18N.th_category || 'Category') + '</th>'
+ '<th>' + (I18N.th_status || 'Status') + '</th>'
+ '<th>' + (I18N.th_chunks || 'Passages') + '</th>'
+ '<th>' + (I18N.th_added || 'Added') + '</th>'
+ '</tr></thead>';
const tbody = document.createElement('tbody');
@@ -120,7 +139,6 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
$wrap.innerHTML = '';
$wrap.appendChild(table);
// Wire selection
const all = document.getElementById('docSelectAll');
all.addEventListener('change', () => {
tbody.querySelectorAll('.doc-check').forEach(c => {
@@ -140,7 +158,8 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
const from = state.offset + 1;
const to = Math.min(state.offset + docs.length, state.total);
$pl.textContent = 'Viser ' + from + '' + to + ' av ' + state.total;
const tmpl = I18N.pager_showing || 'Showing {from}{to} of {total}';
$pl.textContent = tmpl.replace('{from}', from).replace('{to}', to).replace('{total}', state.total);
$prev.disabled = state.offset === 0;
$next.disabled = state.offset + PAGE >= state.total;
$pager.hidden = false;
@@ -148,7 +167,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function updateBulk() {
$bulk.disabled = state.selected.size === 0;
$bulk.textContent = state.selected.size > 0 ? 'Slett valgte (' + state.selected.size + ')' : 'Slett valgte';
$bulk.textContent = state.selected.size > 0
? (I18N.delete_n_selected || 'Delete selected ({n})').replace('{n}', state.selected.size)
: (I18N.delete_selected || 'Delete selected');
}
$prev.addEventListener('click', () => { state.offset = Math.max(0, state.offset - PAGE); load(); });
@@ -163,7 +184,8 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
$bulk.addEventListener('click', () => {
if (!state.selected.size) return;
if (!confirm('Slette ' + state.selected.size + ' dokumenter? Dette kan ikke angres.')) return;
const msg = (I18N.delete_docs_confirm || 'Delete {n} documents? This cannot be undone.').replace('{n}', state.selected.size);
if (!confirm(msg)) return;
const ids = Array.from(state.selected);
$bulk.disabled = true;
fetch(api + '/documents.php?action=delete', {
@@ -174,12 +196,12 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
})
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Slett feilet');
if (!data.ok) throw new Error(data.error?.message || 'Delete failed');
state.selected.clear();
updateBulk();
load();
})
.catch(err => alert('Feil: ' + err.message));
.catch(err => alert(err.message));
});
load();