added translations export and import

This commit is contained in:
2026-05-27 10:53:05 +02:00
parent 9a75577f14
commit 0f89ed6d6f
3 changed files with 293 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import { apiGet, apiPut, apiDelete } from '../api.js';
import { showToast } from '../app.js';
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
import { canEdit, showToast } from '../app.js';
import {
SOURCE_LANG,
normalizeEntry,
@ -23,10 +23,20 @@ export async function translationsPage() {
app.innerHTML = `
<div class="page-header">
<h1>Translations</h1>
<div class="actions" id="transHeaderActions"></div>
</div>
<div id="transPageRoot"><div class="spinner"></div></div>
`;
if (canEdit()) {
document.getElementById('transHeaderActions').innerHTML = `
<input type="file" id="importTransBundleFile" accept=".json,application/json" hidden>
<button type="button" class="btn" id="importTransBundleBtn">Import translations</button>
<button type="button" class="btn btn-primary" id="exportTransBundleBtn">Export translations</button>
`;
bindTranslationBundleActions();
}
try {
renderShell();
await loadTranslations();
@ -387,3 +397,85 @@ function applyFilters() {
}
}
}
function bindTranslationBundleActions() {
const fileInput = document.getElementById('importTransBundleFile');
const importBtn = document.getElementById('importTransBundleBtn');
const exportBtn = document.getElementById('exportTransBundleBtn');
if (!fileInput || !importBtn || !exportBtn) return;
exportBtn.addEventListener('click', async () => {
exportBtn.disabled = true;
const prev = exportBtn.textContent;
exportBtn.textContent = 'Preparing…';
try {
const token = localStorage.getItem('qdb_token');
const res = await fetch('../api/translations?exportBundle=1', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || err.error || 'Export failed');
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const stamp = new Date().toISOString().slice(0, 10);
a.download = `translations_bundle_${stamp}.json`;
a.click();
URL.revokeObjectURL(url);
showToast('Translations bundle downloaded', 'success');
} catch (e) {
showToast(e.message, 'error');
} finally {
exportBtn.disabled = false;
exportBtn.textContent = prev;
}
});
importBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
fileInput.value = '';
if (!file) return;
let bundle;
try {
bundle = JSON.parse(await file.text());
} catch {
showToast('Invalid JSON file', 'error');
return;
}
if (!bundle?.entries || !Array.isArray(bundle.entries)) {
showToast('Not a translations bundle (missing entries array)', 'error');
return;
}
const count = bundle.entries.length;
if (!confirm(`Import translations for ${count} entries from this file?`)) return;
importBtn.disabled = true;
const prev = importBtn.textContent;
importBtn.textContent = 'Importing…';
try {
const result = await apiPost('translations.php', {
action: 'importBundle',
bundle,
});
const imported = result.imported ?? 0;
const skipped = result.skipped ?? 0;
let msg = `Imported ${imported} translation(s)`;
if (skipped) msg += ` (${skipped} skipped)`;
showToast(msg, 'success');
await loadTranslations();
} catch (e) {
showToast(e.message, 'error');
} finally {
importBtn.disabled = false;
importBtn.textContent = prev;
}
});
}