{% extends "dashboard/base.html" %} {% load stapel_translate %} {% block title %}{{ translation.key }} - Translator{% endblock %} {% block extra_css %} {% endblock %} {% block breadcrumb %} {% endblock %} {% block content %}
{% if translation.screenshot %}
Screenshot
{% endif %}
{{ current_position }} / {{ total_count }} {% if is_privileged %} {% endif %}
{% if translation.screenshot %}
Screenshot
{% endif %}
Context {% if translation.refs %} {{ translation.refs|length }} ref{{ translation.refs|length|pluralize }} {% endif %} {% if translation.source %} {{ translation.source }} {% endif %}
{{ translation.key }}
{% if translation.refs %}
{% for ref in translation.refs %} {% if is_privileged %} {{ ref }} {% else %} {{ ref }} {% endif %} {% endfor %}
{% endif %} {% if translation.comment %}
{{ translation.comment }}
{% endif %}
Edit Translation {% if not show_lang_tabs %} {{ current_lang_name }} {% endif %}
{% if show_lang_tabs %}
{% for lang in languages %} {{ lang.code }} {% if lang.verified %} {% endif %} {% endfor %}
{% endif %}
{% csrf_token %}
{% if prev_id %} {% endif %} {% if not current_verified %} {% if next_id %} {% endif %} {% else %} {% endif %} {% if next_id %} {% endif %}
All Translations
{% for item in all_translations %}
{{ item.lang }} {% if item.verified %} Verified {% elif item.value %} Unverified {% else %} Empty {% endif %} {% if item.value %}{{ item.value }}{% else %}—{% endif %}
{% endfor %}
{% if is_privileged %} {% endif %} {% if translation.screenshot %}
Screenshot
{% endif %} const csrfToken = '{{ csrf_token }}'; const translationId = {{ translation.id }}; const currentLang = '{{ current_lang }}'; const isPrivileged = {{ is_privileged|yesno:"true,false" }}; async function safeFetchJson(response) { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { return response.json(); } throw new Error(`Server error (${response.status})`); } // Delete modal function openDeleteModal() { document.getElementById('delete-modal').classList.add('active'); } function closeDeleteModal() { document.getElementById('delete-modal').classList.remove('active'); } async function deleteTranslation() { try { const response = await fetch(`/translate/api/dashboard/translations/${translationId}/`, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken, }, credentials: 'include', }); if (response.ok) { // Redirect to language list window.location.href = `/translate/admin/dashboard/languages/${currentLang}/`; } else { const data = await safeFetchJson(response); alert(data.error || 'Failed to delete translation'); } } catch (err) { alert('Network error: ' + err.message); } } async function changeSourceFilter(source) { // When source changes, redirect to the first translation in that source // (or first in the filtered list with current verified filter) const lang = currentLang; const urlParams = new URL(window.location.href).searchParams; const verifiedParam = urlParams.get('verified'); const sortParam = urlParams.get('sort'); let apiUrl = `/translate/api/dashboard/languages/${lang}/?`; const params = []; if (source) params.push(`source=${encodeURIComponent(source)}`); if (verifiedParam) params.push(`verified=${verifiedParam}`); apiUrl += params.join('&'); try { const response = await fetch(apiUrl, { headers: { 'X-CSRFToken': csrfToken }, credentials: 'include', }); if (response.ok) { const data = await safeFetchJson(response); if (data.length > 0) { // Navigate to first translation in filtered list const firstId = data[0].id; let newUrl = `/translate/admin/dashboard/translations/${firstId}/?lang=${lang}`; if (source) newUrl += `&source=${encodeURIComponent(source)}`; if (verifiedParam) newUrl += `&verified=${verifiedParam}`; if (sortParam && sortParam !== 'default') newUrl += `&sort=${encodeURIComponent(sortParam)}`; window.location.href = newUrl; return; } } } catch (err) { console.error('Failed to get translations:', err); } // Fallback: go to language list page let fallbackUrl = `/translate/admin/dashboard/languages/${lang}/?`; if (source) fallbackUrl += `source=${encodeURIComponent(source)}`; window.location.href = fallbackUrl; } let lastSingleSuggestion = ''; let lastAllSuggestions = {}; function openLlmModal() { document.getElementById('llm-modal').classList.add('active'); document.getElementById('llm-prompt').focus(); // Reset state document.getElementById('llm-single-result').style.display = 'none'; document.getElementById('llm-all-results').style.display = 'none'; document.getElementById('llm-error-modal').style.display = 'none'; document.getElementById('btn-apply-single').style.display = 'none'; if (isPrivileged) { document.getElementById('btn-apply-all').style.display = 'none'; } } function closeLlmModal() { document.getElementById('llm-modal').classList.remove('active'); } async function translateSingle() { const prompt = document.getElementById('llm-prompt').value; const loading = document.getElementById('llm-loading'); const errorDiv = document.getElementById('llm-error-modal'); const resultDiv = document.getElementById('llm-single-result'); const allResultsDiv = document.getElementById('llm-all-results'); const btnApplySingle = document.getElementById('btn-apply-single'); const spinner = document.getElementById('spinner-single'); loading.style.display = 'block'; errorDiv.style.display = 'none'; resultDiv.style.display = 'none'; allResultsDiv.style.display = 'none'; btnApplySingle.style.display = 'none'; spinner.style.display = 'inline-block'; try { const response = await fetch('{% url "dashboard-llm-help" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify({ translation_id: translationId, target_lang: currentLang, prompt: prompt, }), }); const data = await safeFetchJson(response); if (response.ok && data.suggestion) { lastSingleSuggestion = data.suggestion; document.getElementById('llm-single-text').textContent = data.suggestion; resultDiv.style.display = 'block'; btnApplySingle.style.display = 'inline-flex'; } else { errorDiv.textContent = data.error || 'Failed to get suggestion'; errorDiv.style.display = 'block'; } } catch (err) { errorDiv.textContent = err.message; errorDiv.style.display = 'block'; } finally { loading.style.display = 'none'; spinner.style.display = 'none'; } } async function translateAll() { if (!isPrivileged) return; const prompt = document.getElementById('llm-prompt').value; const loading = document.getElementById('llm-loading'); const errorDiv = document.getElementById('llm-error-modal'); const resultDiv = document.getElementById('llm-single-result'); const allResultsDiv = document.getElementById('llm-all-results'); const btnApplyAll = document.getElementById('btn-apply-all'); const spinner = document.getElementById('spinner-all'); loading.style.display = 'block'; errorDiv.style.display = 'none'; resultDiv.style.display = 'none'; allResultsDiv.style.display = 'none'; btnApplyAll.style.display = 'none'; spinner.style.display = 'inline-block'; try { const response = await fetch('{% url "dashboard-llm-help" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify({ translation_id: translationId, translate_all: true, prompt: prompt, }), }); const data = await safeFetchJson(response); if (response.ok && data.suggestions) { lastAllSuggestions = data.suggestions; // Model output is attacker-reachable (it echoes stored source // text and a caller-supplied prompt) — it is rendered as text. const rows = []; for (const [lang, value] of Object.entries(data.suggestions)) { const row = stapelDom.el('div', 'llm-result'); row.appendChild(stapelDom.el('div', 'llm-result-lang', lang)); row.appendChild(stapelDom.el('div', 'llm-result-text', value || 'Empty')); rows.push(row); } stapelDom.replace(allResultsDiv, rows); allResultsDiv.style.display = 'block'; btnApplyAll.style.display = 'inline-flex'; } else { errorDiv.textContent = data.error || 'Failed to get suggestions'; errorDiv.style.display = 'block'; } } catch (err) { errorDiv.textContent = err.message; errorDiv.style.display = 'block'; } finally { loading.style.display = 'none'; spinner.style.display = 'none'; } } async function applySingleAndReload() { if (!lastSingleSuggestion) return; const prompt = document.getElementById('llm-prompt').value; const loading = document.getElementById('llm-loading'); const errorDiv = document.getElementById('llm-error-modal'); loading.style.display = 'block'; errorDiv.style.display = 'none'; try { const response = await fetch('{% url "dashboard-llm-help" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify({ translation_id: translationId, target_lang: currentLang, prompt: prompt, apply: true, }), }); const data = await safeFetchJson(response); if (response.ok && data.applied) { window.location.reload(); } else { errorDiv.textContent = data.error || 'Failed to apply translation'; errorDiv.style.display = 'block'; loading.style.display = 'none'; } } catch (err) { errorDiv.textContent = err.message; errorDiv.style.display = 'block'; loading.style.display = 'none'; } } async function applyAllAndClose() { if (!isPrivileged || !Object.keys(lastAllSuggestions).length) return; const prompt = document.getElementById('llm-prompt').value; const loading = document.getElementById('llm-loading'); const errorDiv = document.getElementById('llm-error-modal'); loading.style.display = 'block'; errorDiv.style.display = 'none'; try { const response = await fetch('{% url "dashboard-llm-help" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify({ translation_id: translationId, translate_all: true, prompt: prompt, apply: true, }), }); const data = await safeFetchJson(response); if (response.ok && data.applied) { // Reload page to show updated translations window.location.reload(); } else { errorDiv.textContent = data.error || 'Failed to apply translations'; errorDiv.style.display = 'block'; loading.style.display = 'none'; } } catch (err) { errorDiv.textContent = err.message; errorDiv.style.display = 'block'; loading.style.display = 'none'; } } // Screenshot lightbox function openScreenshot() { const el = document.getElementById('screenshot-lightbox'); if (el) el.classList.add('active'); } function closeScreenshot() { const el = document.getElementById('screenshot-lightbox'); if (el) el.classList.remove('active'); } stapelDom.register({ openScreenshot: openScreenshot, closeScreenshot: closeScreenshot, closeScreenshotOnOverlay: (node, event) => { if (event.target === node) closeScreenshot(); }, openDeleteModal: openDeleteModal, closeDeleteModal: closeDeleteModal, deleteTranslation: deleteTranslation, openLlmModal: openLlmModal, closeLlmModal: closeLlmModal, translateSingle: translateSingle, translateAll: translateAll, applySingleAndReload: applySingleAndReload, applyAllAndClose: applyAllAndClose, changeSourceFilter: (node) => changeSourceFilter(node.value), }); // Close modals on Escape document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeScreenshot(); closeLlmModal(); if (isPrivileged) closeDeleteModal(); } // Ctrl/Cmd + Enter to save form if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && !document.getElementById('llm-modal').classList.contains('active')) { e.preventDefault(); document.getElementById('translation-form').submit(); } // Arrow navigation (when not in input/textarea and modal not open) if (['TEXTAREA', 'INPUT'].includes(document.activeElement.tagName)) return; if (document.getElementById('llm-modal').classList.contains('active')) return; if (e.key === 'ArrowLeft') { const prevLink = document.querySelector('.nav-prev'); if (prevLink) prevLink.click(); } if (e.key === 'ArrowRight') { const nextLink = document.querySelector('.nav-next'); if (nextLink) nextLink.click(); } }); // Close modal on overlay click document.getElementById('llm-modal').addEventListener('click', function(e) { if (e.target === this) closeLlmModal(); }); // Auto-save translator comment let commentSaveTimeout = null; const commentTextarea = document.getElementById('translator-comment'); const commentSaveStatus = document.getElementById('comment-save-status'); function saveTranslatorComment() { const comment = commentTextarea.value; commentSaveStatus.textContent = 'Saving...'; commentSaveStatus.className = 'save-status saving'; fetch(`/translate/api/dashboard/translations/${translationId}/comment/`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, credentials: 'include', body: JSON.stringify({ translator_comment: comment }), }) .then(response => { if (response.ok) { commentSaveStatus.textContent = 'Saved'; commentSaveStatus.className = 'save-status saved'; setTimeout(() => { commentSaveStatus.textContent = ''; }, 2000); } else { throw new Error('Failed to save'); } }) .catch(err => { commentSaveStatus.textContent = 'Error saving'; commentSaveStatus.className = 'save-status error'; }); } commentTextarea.addEventListener('input', function() { if (commentSaveTimeout) { clearTimeout(commentSaveTimeout); } commentSaveStatus.textContent = ''; commentSaveTimeout = setTimeout(saveTranslatorComment, 1000); }); // Save on blur too commentTextarea.addEventListener('blur', function() { if (commentSaveTimeout) { clearTimeout(commentSaveTimeout); saveTranslatorComment(); } }); {% endblock %}