Merge pull request #746 from christianlouis/copilot/localize-view-links

feat(i18n): localize /files/ view
This commit is contained in:
Christian Krakau-Louis
2026-03-16 19:24:56 +01:00
committed by GitHub
2 changed files with 219 additions and 126 deletions
+104 -56
View File
@@ -716,19 +716,19 @@
<td>{{ file.mime_type }}</td> <td>{{ file.mime_type }}</td>
<td> <td>
<span class="status-badge status-{{ file.processing_status }}"> <span class="status-badge status-{{ file.processing_status }}">
{{ file.processing_status | title }} {% if file.processing_status == 'pending' %}{{ _("common.pending") }}{% elif file.processing_status == 'processing' %}{{ _("common.processing") }}{% elif file.processing_status == 'completed' %}{{ _("common.completed") }}{% elif file.processing_status == 'failed' %}{{ _("common.failed") }}{% elif file.processing_status == 'duplicate' %}{{ _("files.status_duplicate") }}{% else %}{{ file.processing_status | title }}{% endif %}
</span> </span>
</td> </td>
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else 'N/A' }}</td> <td>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else _('common.not_available') }}</td>
<td> <td>
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<button onclick="openPreviewModal({{ file.id }}, '{{ file.mime_type or '' }}', '{{ file.original_filename | e }}', event)" class="action-btn" title="Quick preview" aria-label="Preview {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="openPreviewModal({{ file.id }}, '{{ file.mime_type or '' }}', '{{ file.original_filename | e }}', event)" class="action-btn" title="{{ _('files.action_preview') }}" aria-label="{{ _('files.action_preview') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-eye" aria-hidden="true"></i> <i class="fas fa-eye" aria-hidden="true"></i>
</button> </button>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="{{ _('files.action_details') }}" aria-label="{{ _('files.action_details') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i> <i class="fas fa-info-circle" aria-hidden="true"></i>
</button> </button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="{{ _('files.action_delete') }}" aria-label="{{ _('files.action_delete') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
</div> </div>
@@ -752,25 +752,25 @@
tabindex="0" tabindex="0"
onclick="viewFileDetail({{ file.id }}, event)" onclick="viewFileDetail({{ file.id }}, event)"
onkeydown="if(event.key==='Enter'||event.key===' '){viewFileDetail({{ file.id }}, event);}" onkeydown="if(event.key==='Enter'||event.key===' '){viewFileDetail({{ file.id }}, event);}"
aria-label="View details for {{ file.original_filename }}" aria-label="{{ _('files.action_details') }} {{ file.original_filename }}"
> >
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="font-semibold text-gray-800 truncate">{{ file.original_filename }}</p> <p class="font-semibold text-gray-800 truncate">{{ file.original_filename }}</p>
<p class="text-xs text-gray-500 mt-1">ID: {{ file.id }} &middot; {{ (file.file_size / 1024) | round(2) }} KB</p> <p class="text-xs text-gray-500 mt-1">ID: {{ file.id }} &middot; {{ (file.file_size / 1024) | round(2) }} KB</p>
<p class="text-xs text-gray-500 mt-0.5">{{ file.mime_type }}</p> <p class="text-xs text-gray-500 mt-0.5">{{ file.mime_type }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else 'N/A' }}</p> <p class="text-xs text-gray-400 mt-0.5">{{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else _('common.not_available') }}</p>
</div> </div>
<div class="flex flex-col items-end gap-2 flex-shrink-0"> <div class="flex flex-col items-end gap-2 flex-shrink-0">
<span class="status-badge status-{{ file.processing_status }}">{{ file.processing_status | title }}</span> <span class="status-badge status-{{ file.processing_status }}">{% if file.processing_status == 'pending' %}{{ _("common.pending") }}{% elif file.processing_status == 'processing' %}{{ _("common.processing") }}{% elif file.processing_status == 'completed' %}{{ _("common.completed") }}{% elif file.processing_status == 'failed' %}{{ _("common.failed") }}{% elif file.processing_status == 'duplicate' %}{{ _("files.status_duplicate") }}{% else %}{{ file.processing_status | title }}{% endif %}</span>
<div class="flex gap-1" role="group" aria-label="File actions" onclick="event.stopPropagation();" onkeydown="event.stopPropagation();"> <div class="flex gap-1" role="group" aria-label="{{ _('files.file_actions_aria') }}" onclick="event.stopPropagation();" onkeydown="event.stopPropagation();">
<button onclick="openPreviewModal({{ file.id }}, '{{ file.mime_type or '' }}', '{{ file.original_filename | e }}', event)" class="action-btn" title="Quick preview" aria-label="Preview {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="openPreviewModal({{ file.id }}, '{{ file.mime_type or '' }}', '{{ file.original_filename | e }}', event)" class="action-btn" title="{{ _('files.action_preview') }}" aria-label="{{ _('files.action_preview') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-eye" aria-hidden="true"></i> <i class="fas fa-eye" aria-hidden="true"></i>
</button> </button>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="{{ _('files.action_details') }}" aria-label="{{ _('files.action_details') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i> <i class="fas fa-info-circle" aria-hidden="true"></i>
</button> </button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;"> <button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="{{ _('files.action_delete') }}" aria-label="{{ _('files.action_delete') }} {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
</div> </div>
@@ -841,6 +841,54 @@
</div> </div>
<script> <script>
// ── Translations for JavaScript ──
const __i18n = {
previewFallback: {{ _("files.js_preview_fallback") | tojson }},
fullView: {{ _("files.js_full_view") | tojson }},
download: {{ _("files.js_download") | tojson }},
loading: {{ _("files.js_loading") | tojson }},
failedLoadPdf: {{ _("files.js_failed_load_pdf") | tojson }},
pdfPageInfo: {{ _("files.js_pdf_page_info") | tojson }},
failedLoadFile: {{ _("files.js_failed_load_file") | tojson }},
openFullViewAria: {{ _("files.js_open_full_view_aria") | tojson }},
downloadFileAria: {{ _("files.js_download_file_aria") | tojson }},
failedDeleteFile: {{ _("files.js_failed_delete_file") | tojson }},
errorDeletingFile: {{ _("files.js_error_deleting_file") | tojson }},
noSavedSearches: {{ _("files.js_no_saved_searches") | tojson }},
loadSavedSearchesError: {{ _("files.js_load_saved_searches_error") | tojson }},
noFiltersToSave: {{ _("files.js_no_filters_to_save") | tojson }},
enterSearchName: {{ _("files.js_enter_search_name") | tojson }},
deleteSavedSearchConfirm: {{ _("files.js_delete_saved_search_confirm") | tojson }},
deleteSavedSearchAria: {{ _("files.js_delete_saved_search_aria") | tojson }},
selectFilesDelete: {{ _("files.js_select_files_delete") | tojson }},
confirmDeleteFiles: {{ _("files.js_confirm_delete_files") | tojson }},
failedDeleteFiles: {{ _("files.js_failed_delete_files") | tojson }},
errorDeletingFiles: {{ _("files.js_error_deleting_files") | tojson }},
selectFilesReprocess: {{ _("files.js_select_files_reprocess") | tojson }},
confirmReprocessFiles: {{ _("files.js_confirm_reprocess_files") | tojson }},
failedReprocessFiles: {{ _("files.js_failed_reprocess_files") | tojson }},
errorReprocessingFiles: {{ _("files.js_error_reprocessing_files") | tojson }},
selectFilesCloudOcr: {{ _("files.js_select_files_cloud_ocr") | tojson }},
confirmCloudOcr: {{ _("files.js_confirm_cloud_ocr") | tojson }},
failedQueueCloudOcr: {{ _("files.js_failed_queue_cloud_ocr") | tojson }},
errorQueuingCloudOcr: {{ _("files.js_error_queuing_cloud_ocr") | tojson }},
selectFilesDownload: {{ _("files.js_select_files_download") | tojson }},
failedCreateZip: {{ _("files.js_failed_create_zip") | tojson }},
errorDownloadingFiles: {{ _("files.js_error_downloading_files") | tojson }},
searching: {{ _("files.js_searching") | tojson }},
searchUnavailable: {{ _("files.js_search_unavailable") | tojson }},
searchResultsCount: {{ _("files.js_search_results_count") | tojson }},
noResults: {{ _("files.js_no_results") | tojson }},
untitled: {{ _("files.js_untitled") | tojson }},
viewFile: {{ _("files.js_view_file") | tojson }},
prevPage: {{ _("files.js_prev_page") | tojson }},
pageInfo: {{ _("files.js_page_info") | tojson }},
nextPage: {{ _("files.js_next_page") | tojson }},
zoomIn: {{ _("files.js_zoom_in") | tojson }},
zoomOut: {{ _("files.js_zoom_out") | tojson }},
resetZoom: {{ _("files.js_reset_zoom") | tojson }},
};
// Modal functionality // Modal functionality
const deleteModal = document.getElementById('deleteModal'); const deleteModal = document.getElementById('deleteModal');
const cancelDelete = document.getElementById('cancelDelete'); const cancelDelete = document.getElementById('cancelDelete');
@@ -868,7 +916,7 @@
const title = document.getElementById('previewTitle'); const title = document.getElementById('previewTitle');
const body = document.getElementById('previewBody'); const body = document.getElementById('previewBody');
const footer = document.getElementById('previewFooter'); const footer = document.getElementById('previewFooter');
title.textContent = filename || 'Preview'; title.textContent = filename || __i18n.previewFallback;
body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:300px;color:#9ca3af;"><i class="fas fa-spinner fa-spin" style="font-size:2rem;"></i></div>'; body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:300px;color:#9ca3af;"><i class="fas fa-spinner fa-spin" style="font-size:2rem;"></i></div>';
footer.innerHTML = ''; footer.innerHTML = '';
_pmPdf = { doc: null, page: 1, total: 0 }; _pmPdf = { doc: null, page: 1, total: 0 };
@@ -877,8 +925,8 @@
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
footer.innerHTML = footer.innerHTML =
`<a href="/files/${fileId}" aria-label="Open full view"><i class="fas fa-expand" aria-hidden="true"></i> Full view</a>` + `<a href="/files/${fileId}" aria-label="${__i18n.openFullViewAria}"><i class="fas fa-expand" aria-hidden="true"></i> ${__i18n.fullView}</a>` +
`<a href="/api/files/${fileId}/download?version=processed" aria-label="Download file"><i class="fas fa-download" aria-hidden="true"></i> Download</a>`; `<a href="/api/files/${fileId}/download?version=processed" aria-label="${__i18n.downloadFileAria}"><i class="fas fa-download" aria-hidden="true"></i> ${__i18n.download}</a>`;
// Try "processed" first; fall back to "original" on 404 // Try "processed" first; fall back to "original" on 404
_pmLoadPreview(body, fileId, mime, filename, 'processed'); _pmLoadPreview(body, fileId, mime, filename, 'processed');
@@ -911,7 +959,7 @@
body.innerHTML = body.innerHTML =
'<div class="pm-pdf-controls">' + '<div class="pm-pdf-controls">' +
'<button onclick="_pmPdfPage(-1)" id="pm-prev" aria-label="Previous page"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>' + '<button onclick="_pmPdfPage(-1)" id="pm-prev" aria-label="Previous page"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>' +
'<span id="pm-page-info" style="font-size:0.8rem;min-width:100px;text-align:center;">Loading…</span>' + '<span id="pm-page-info" style="font-size:0.8rem;min-width:100px;text-align:center;">' + __i18n.loading + '</span>' +
'<button onclick="_pmPdfPage(1)" id="pm-next" aria-label="Next page"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>' + '<button onclick="_pmPdfPage(1)" id="pm-next" aria-label="Next page"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>' +
'</div>' + '</div>' +
'<div class="pm-canvas-wrap" id="pm-canvas-wrap"><div style="text-align:center;padding:3rem;color:#94a3b8;"><i class="fas fa-spinner fa-spin" style="font-size:2rem;"></i></div></div>'; '<div class="pm-canvas-wrap" id="pm-canvas-wrap"><div style="text-align:center;padding:3rem;color:#94a3b8;"><i class="fas fa-spinner fa-spin" style="font-size:2rem;"></i></div></div>';
@@ -923,7 +971,7 @@
_pmPdfRender(); _pmPdfRender();
}).catch(function() { }).catch(function() {
if (onError) { onError(); return; } if (onError) { onError(); return; }
document.getElementById('pm-canvas-wrap').innerHTML = '<div style="text-align:center;padding:2rem;color:#f87171;"><i class="fas fa-exclamation-triangle" style="font-size:2rem;margin-bottom:0.5rem;"></i><p>Failed to load PDF</p></div>'; document.getElementById('pm-canvas-wrap').innerHTML = '<div style="text-align:center;padding:2rem;color:#f87171;"><i class="fas fa-exclamation-triangle" style="font-size:2rem;margin-bottom:0.5rem;"></i><p>' + __i18n.failedLoadPdf + '</p></div>';
}); });
} }
@@ -953,7 +1001,7 @@
function _pmPdfUpdateInfo() { function _pmPdfUpdateInfo() {
var info = document.getElementById('pm-page-info'); var info = document.getElementById('pm-page-info');
if (info) info.textContent = 'Page ' + _pmPdf.page + ' of ' + _pmPdf.total; if (info) info.textContent = __i18n.pdfPageInfo.replace('{page}', _pmPdf.page).replace('{total}', _pmPdf.total);
var prev = document.getElementById('pm-prev'); var prev = document.getElementById('pm-prev');
var next = document.getElementById('pm-next'); var next = document.getElementById('pm-next');
if (prev) prev.disabled = _pmPdf.page <= 1; if (prev) prev.disabled = _pmPdf.page <= 1;
@@ -964,12 +1012,12 @@
function _pmRenderImage(body, url, alt) { function _pmRenderImage(body, url, alt) {
body.innerHTML = body.innerHTML =
'<div class="pm-img-controls">' + '<div class="pm-img-controls">' +
'<button onclick="_pmImgZoom(1.25)" title="Zoom in" aria-label="Zoom in"><i class="fas fa-search-plus" aria-hidden="true"></i></button>' + '<button onclick="_pmImgZoom(1.25)" title="' + __i18n.zoomIn + '" aria-label="' + __i18n.zoomIn + '"><i class="fas fa-search-plus" aria-hidden="true"></i></button>' +
'<button onclick="_pmImgZoom(0.8)" title="Zoom out" aria-label="Zoom out"><i class="fas fa-search-minus" aria-hidden="true"></i></button>' + '<button onclick="_pmImgZoom(0.8)" title="' + __i18n.zoomOut + '" aria-label="' + __i18n.zoomOut + '"><i class="fas fa-search-minus" aria-hidden="true"></i></button>' +
'<button onclick="_pmImgReset()" title="Reset" aria-label="Reset zoom"><i class="fas fa-expand" aria-hidden="true"></i></button>' + '<button onclick="_pmImgReset()" title="' + __i18n.resetZoom + '" aria-label="' + __i18n.resetZoom + '"><i class="fas fa-expand" aria-hidden="true"></i></button>' +
'<span id="pm-zoom-level" style="font-size:0.8rem;color:#6b7280;">100%</span>' + '<span id="pm-zoom-level" style="font-size:0.8rem;color:#6b7280;">100%</span>' +
'</div>' + '</div>' +
'<div class="pm-img-wrap" id="pm-img-wrap"><img id="pm-preview-img" src="' + url + '" alt="' + (alt || 'Preview') + '" draggable="false" style="max-width:100%;height:auto;"></div>'; '<div class="pm-img-wrap" id="pm-img-wrap"><img id="pm-preview-img" src="' + url + '" alt="' + (alt || __i18n.previewFallback) + '" draggable="false" style="max-width:100%;height:auto;"></div>';
// Drag-pan // Drag-pan
var wrap = document.getElementById('pm-img-wrap'); var wrap = document.getElementById('pm-img-wrap');
if (wrap) { if (wrap) {
@@ -1000,7 +1048,7 @@
// Text rendering inside preview modal // Text rendering inside preview modal
function _pmRenderText(body, url, onError) { function _pmRenderText(body, url, onError) {
body.innerHTML = '<div class="pm-text-content" id="pm-text-content"><div style="text-align:center;padding:2rem;color:#94a3b8;"><i class="fas fa-spinner fa-spin"></i> Loading…</div></div>'; body.innerHTML = '<div class="pm-text-content" id="pm-text-content"><div style="text-align:center;padding:2rem;color:#94a3b8;"><i class="fas fa-spinner fa-spin"></i> ' + __i18n.loading + '</div></div>';
fetch(url) fetch(url)
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(text) { .then(function(text) {
@@ -1021,7 +1069,7 @@
.catch(function() { .catch(function() {
if (onError) { onError(); return; } if (onError) { onError(); return; }
var c = document.getElementById('pm-text-content'); var c = document.getElementById('pm-text-content');
if (c) c.innerHTML = '<div style="color:#f87171;padding:1rem;">Failed to load file</div>'; if (c) c.innerHTML = '<div style="color:#f87171;padding:1rem;">' + __i18n.failedLoadFile + '</div>';
}); });
} }
@@ -1065,7 +1113,7 @@
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) { if (contentType && contentType.includes('application/json')) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(err.detail || 'Failed to delete file'); throw new Error(err.detail || __i18n.failedDeleteFile);
}); });
} else { } else {
// Non-JSON response (likely HTML error page) // Non-JSON response (likely HTML error page)
@@ -1084,7 +1132,7 @@
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert(`Error deleting file: ${error.message}`); alert(__i18n.errorDeletingFile.replace('{error}', error.message));
}); });
} }
@@ -1124,14 +1172,14 @@
const container = document.getElementById('saved-searches-list'); const container = document.getElementById('saved-searches-list');
if (!container) return; if (!container) return;
if (!searches || searches.length === 0) { if (!searches || searches.length === 0) {
container.innerHTML = '<span style="color: var(--text-muted); font-size: 0.85rem;">No saved searches yet</span>'; container.innerHTML = '<span style="color: var(--text-muted); font-size: 0.85rem;">' + __i18n.noSavedSearches + '</span>';
return; return;
} }
container.innerHTML = searches.map(s => { container.innerHTML = searches.map(s => {
const params = new URLSearchParams(s.filters); const params = new URLSearchParams(s.filters);
return `<span class="saved-search-tag" style="display: inline-flex; align-items: center; gap: 0.25rem; background: var(--bg-tertiary, #e5e7eb); padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-size: 0.8rem;"> return `<span class="saved-search-tag" style="display: inline-flex; align-items: center; gap: 0.25rem; background: var(--bg-tertiary, #e5e7eb); padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-size: 0.8rem;">
<a href="/files?${params.toString()}" style="text-decoration: none; color: inherit;">${s.name}</a> <a href="/files?${params.toString()}" style="text-decoration: none; color: inherit;">${s.name}</a>
<button type="button" onclick="deleteSavedSearch(${s.id})" style="border: none; background: none; cursor: pointer; color: var(--text-muted); padding: 0; line-height: 1;" aria-label="Delete saved search ${s.name}"> <button type="button" onclick="deleteSavedSearch(${s.id})" style="border: none; background: none; cursor: pointer; color: var(--text-muted); padding: 0; line-height: 1;" aria-label="${__i18n.deleteSavedSearchAria.replace('{name}', s.name)}">
<i class="fas fa-times" aria-hidden="true"></i> <i class="fas fa-times" aria-hidden="true"></i>
</button> </button>
</span>`; </span>`;
@@ -1139,7 +1187,7 @@
}) })
.catch(() => { .catch(() => {
const container = document.getElementById('saved-searches-list'); const container = document.getElementById('saved-searches-list');
if (container) container.innerHTML = '<span style="color: var(--text-muted); font-size: 0.85rem;">Could not load saved searches</span>'; if (container) container.innerHTML = '<span style="color: var(--text-muted); font-size: 0.85rem;">' + __i18n.loadSavedSearchesError + '</span>';
}); });
} }
@@ -1152,10 +1200,10 @@
if (val) filters[key] = val; if (val) filters[key] = val;
}); });
if (Object.keys(filters).length === 0) { if (Object.keys(filters).length === 0) {
alert('No filters to save. Please apply at least one filter (search, date, status, tags, etc.) before saving.'); alert(__i18n.noFiltersToSave);
return; return;
} }
const name = prompt('Enter a name for this saved search:'); const name = prompt(__i18n.enterSearchName);
if (!name || !name.trim()) return; if (!name || !name.trim()) return;
fetch('/api/saved-searches', { fetch('/api/saved-searches', {
method: 'POST', method: 'POST',
@@ -1171,7 +1219,7 @@
} }
function deleteSavedSearch(id) { function deleteSavedSearch(id) {
if (!confirm('Delete this saved search?')) return; if (!confirm(__i18n.deleteSavedSearchConfirm)) return;
fetch(`/api/saved-searches/${id}`, { method: 'DELETE' }) fetch(`/api/saved-searches/${id}`, { method: 'DELETE' })
.then(response => { .then(response => {
if (!response.ok) throw new Error('Failed to delete'); if (!response.ok) throw new Error('Failed to delete');
@@ -1226,11 +1274,11 @@
function bulkDelete() { function bulkDelete() {
const fileIds = getSelectedFileIds(); const fileIds = getSelectedFileIds();
if (fileIds.length === 0) { if (fileIds.length === 0) {
alert('Please select files to delete'); alert(__i18n.selectFilesDelete);
return; return;
} }
if (!confirm(`Are you sure you want to delete ${fileIds.length} file(s)?`)) { if (!confirm(__i18n.confirmDeleteFiles.replace('{count}', fileIds.length))) {
return; return;
} }
@@ -1244,7 +1292,7 @@
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(err.detail || 'Failed to delete files'); throw new Error(err.detail || __i18n.failedDeleteFiles);
}); });
} }
return response.json(); return response.json();
@@ -1255,18 +1303,18 @@
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert(`Error deleting files: ${error.message}`); alert(__i18n.errorDeletingFiles.replace('{error}', error.message));
}); });
} }
function bulkReprocess() { function bulkReprocess() {
const fileIds = getSelectedFileIds(); const fileIds = getSelectedFileIds();
if (fileIds.length === 0) { if (fileIds.length === 0) {
alert('Please select files to reprocess'); alert(__i18n.selectFilesReprocess);
return; return;
} }
if (!confirm(`Are you sure you want to reprocess ${fileIds.length} file(s)?`)) { if (!confirm(__i18n.confirmReprocessFiles.replace('{count}', fileIds.length))) {
return; return;
} }
@@ -1280,7 +1328,7 @@
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(err.detail || 'Failed to reprocess files'); throw new Error(err.detail || __i18n.failedReprocessFiles);
}); });
} }
return response.json(); return response.json();
@@ -1297,18 +1345,18 @@
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert(`Error reprocessing files: ${error.message}`); alert(__i18n.errorReprocessingFiles.replace('{error}', error.message));
}); });
} }
function bulkReprocessCloudOcr() { function bulkReprocessCloudOcr() {
const fileIds = getSelectedFileIds(); const fileIds = getSelectedFileIds();
if (fileIds.length === 0) { if (fileIds.length === 0) {
alert('Please select files to re-run Cloud OCR on'); alert(__i18n.selectFilesCloudOcr);
return; return;
} }
if (!confirm(`Are you sure you want to re-run Cloud OCR on ${fileIds.length} file(s)?`)) { if (!confirm(__i18n.confirmCloudOcr.replace('{count}', fileIds.length))) {
return; return;
} }
@@ -1322,7 +1370,7 @@
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(err.detail || 'Failed to queue Cloud OCR'); throw new Error(err.detail || __i18n.failedQueueCloudOcr);
}); });
} }
return response.json(); return response.json();
@@ -1339,14 +1387,14 @@
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert(`Error queuing Cloud OCR: ${error.message}`); alert(__i18n.errorQueuingCloudOcr.replace('{error}', error.message));
}); });
} }
function bulkDownload() { function bulkDownload() {
const fileIds = getSelectedFileIds(); const fileIds = getSelectedFileIds();
if (fileIds.length === 0) { if (fileIds.length === 0) {
alert('Please select files to download'); alert(__i18n.selectFilesDownload);
return; return;
} }
@@ -1360,7 +1408,7 @@
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(err.detail || 'Failed to create ZIP download'); throw new Error(err.detail || __i18n.failedCreateZip);
}); });
} }
return response.blob(); return response.blob();
@@ -1377,7 +1425,7 @@
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert(`Error downloading files: ${error.message}`); alert(__i18n.errorDownloadingFiles.replace('{error}', error.message));
}); });
} }
@@ -1471,7 +1519,7 @@
const summary = document.getElementById('search-results-summary'); const summary = document.getElementById('search-results-summary');
const pagination = document.getElementById('search-results-pagination'); const pagination = document.getElementById('search-results-pagination');
list.innerHTML = '<div style="padding: 1rem; color: #6b7280; font-size: 0.875rem;"><i class="fas fa-spinner fa-spin"></i> Searching…</div>'; list.innerHTML = '<div style="padding: 1rem; color: #6b7280; font-size: 0.875rem;"><i class="fas fa-spinner fa-spin"></i> ' + __i18n.searching + '</div>';
summary.textContent = ''; summary.textContent = '';
pagination.innerHTML = ''; pagination.innerHTML = '';
panel.style.display = 'block'; panel.style.display = 'block';
@@ -1484,7 +1532,7 @@
}) })
.then(data => renderSearchResults(data, q)) .then(data => renderSearchResults(data, q))
.catch(err => { .catch(err => {
list.innerHTML = `<div style="padding: 1rem; color: #dc2626; font-size: 0.875rem;"><i class="fas fa-exclamation-triangle"></i> Search unavailable: ${err.message}</div>`; list.innerHTML = '<div style="padding: 1rem; color: #dc2626; font-size: 0.875rem;"><i class="fas fa-exclamation-triangle"></i> ' + __i18n.searchUnavailable.replace('{error}', err.message) + '</div>';
summary.textContent = ''; summary.textContent = '';
}); });
} }
@@ -1496,17 +1544,17 @@
const pagination = document.getElementById('search-results-pagination'); const pagination = document.getElementById('search-results-pagination');
const { results, total, page, pages } = data; const { results, total, page, pages } = data;
summary.textContent = `${total} result${total !== 1 ? 's' : ''} for "${q}"`; summary.textContent = __i18n.searchResultsCount.replace('{total}', total).replace('{query}', q);
if (!results || results.length === 0) { if (!results || results.length === 0) {
list.innerHTML = '<div style="padding: 1rem; color: #6b7280; font-size: 0.875rem;">No results found.</div>'; list.innerHTML = '<div style="padding: 1rem; color: #6b7280; font-size: 0.875rem;">' + __i18n.noResults + '</div>';
pagination.innerHTML = ''; pagination.innerHTML = '';
return; return;
} }
list.innerHTML = results.map(hit => { list.innerHTML = results.map(hit => {
const fmt = hit._formatted || {}; const fmt = hit._formatted || {};
const title = fmt.document_title || hit.document_title || hit.original_filename || '(untitled)'; const title = fmt.document_title || hit.document_title || hit.original_filename || __i18n.untitled;
const filename = fmt.original_filename || hit.original_filename || ''; const filename = fmt.original_filename || hit.original_filename || '';
const snippet = fmt.ocr_text || ''; const snippet = fmt.ocr_text || '';
const tags = Array.isArray(hit.tags) ? hit.tags.join(', ') : (hit.tags || ''); const tags = Array.isArray(hit.tags) ? hit.tags.join(', ') : (hit.tags || '');
@@ -1524,7 +1572,7 @@
${snippet ? `<div style="margin-top: 0.4rem; font-size: 0.8rem; color: #374151; white-space: pre-wrap; word-break: break-word;">…${snippet}…</div>` : ''} ${snippet ? `<div style="margin-top: 0.4rem; font-size: 0.8rem; color: #374151; white-space: pre-wrap; word-break: break-word;">…${snippet}…</div>` : ''}
</div> </div>
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
<a href="/files/${hit.file_id}" style="padding: 0.25rem 0.6rem; background: #f3f4f6; color: #374151; border-radius: 0.25rem; font-size: 0.8rem; text-decoration: none; white-space: nowrap;" title="View file"> <a href="/files/${hit.file_id}" style="padding: 0.25rem 0.6rem; background: #f3f4f6; color: #374151; border-radius: 0.25rem; font-size: 0.8rem; text-decoration: none; white-space: nowrap;" title="${__i18n.viewFile}">
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
</div> </div>
@@ -1535,11 +1583,11 @@
if (pages > 1) { if (pages > 1) {
const btns = []; const btns = [];
if (page > 1) { if (page > 1) {
btns.push(`<button onclick="runFullTextSearch(${page - 1})" style="padding: 0.25rem 0.6rem; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.8rem; cursor: pointer; background: white;">« Prev</button>`); btns.push(`<button onclick="runFullTextSearch(${page - 1})" style="padding: 0.25rem 0.6rem; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.8rem; cursor: pointer; background: white;">${__i18n.prevPage}</button>`);
} }
btns.push(`<span style="padding: 0.25rem 0.6rem; font-size: 0.8rem; color: #6b7280;">Page ${page} / ${pages}</span>`); btns.push(`<span style="padding: 0.25rem 0.6rem; font-size: 0.8rem; color: #6b7280;">${__i18n.pageInfo.replace('{page}', page).replace('{pages}', pages)}</span>`);
if (page < pages) { if (page < pages) {
btns.push(`<button onclick="runFullTextSearch(${page + 1})" style="padding: 0.25rem 0.6rem; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.8rem; cursor: pointer; background: white;">Next »</button>`); btns.push(`<button onclick="runFullTextSearch(${page + 1})" style="padding: 0.25rem 0.6rem; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.8rem; cursor: pointer; background: white;">${__i18n.nextPage}</button>`);
} }
pagination.innerHTML = btns.join(''); pagination.innerHTML = btns.join('');
} else { } else {
+115 -70
View File
@@ -41,6 +41,53 @@
"about.story_heading": "Our Story", "about.story_heading": "Our Story",
"about.story_p1": "DocuElevate was created with one goal in mind: to simplify and streamline document management for everyone, whether youre a small startup or a large enterprise.", "about.story_p1": "DocuElevate was created with one goal in mind: to simplify and streamline document management for everyone, whether youre a small startup or a large enterprise.",
"about.story_p2": "We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama, OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.", "about.story_p2": "We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama, OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.",
"admin_files.admin_only_badge": "Admin Only",
"admin_files.aria_breadcrumb": "Breadcrumb",
"admin_files.badge_delta_detected": "Delta detected",
"admin_files.badge_duplicate": "dup",
"admin_files.badge_in_db": "in DB",
"admin_files.badge_on_disk": "on disk",
"admin_files.breadcrumb_workdir": "workdir",
"admin_files.btn_download": "Download",
"admin_files.col_actions": "Actions",
"admin_files.col_db": "DB",
"admin_files.col_health": "Health",
"admin_files.col_id": "ID",
"admin_files.col_ingested": "Ingested",
"admin_files.col_local_filename": "local_filename",
"admin_files.col_missing_paths": "Missing paths",
"admin_files.col_modified": "Modified",
"admin_files.col_name": "Name",
"admin_files.col_original_file_path": "original_file_path",
"admin_files.col_original_filename": "Original Filename",
"admin_files.col_path_relative": "Path (relative to workdir)",
"admin_files.col_processed_file_path": "processed_file_path",
"admin_files.col_size": "Size",
"admin_files.delta_detected_detail": "Found {orphan_count} orphan file(s) on disk with no DB record, and {ghost_count} DB record(s) with missing files on disk.",
"admin_files.delta_detected_title": "Delta detected.",
"admin_files.empty_database": "No file records found in the database.",
"admin_files.empty_directory": "This directory is empty.",
"admin_files.ghost_records_desc": "(in DB, file(s) missing on disk)",
"admin_files.ghost_records_heading": "Ghost records",
"admin_files.heading": "File Manager",
"admin_files.health_missing": "Missing",
"admin_files.health_ok": "OK",
"admin_files.legend_file_exists": "File exists on disk",
"admin_files.legend_file_missing": "File missing from disk",
"admin_files.legend_found_in_db": "Found in DB",
"admin_files.legend_not_in_db": "Not in DB (orphan)",
"admin_files.legend_path_not_set": "Path not set",
"admin_files.no_delta": "No delta found — filesystem and database are in sync.",
"admin_files.no_ghost_records": "No ghost records found.",
"admin_files.no_orphan_files": "No orphan files found.",
"admin_files.orphan_files_desc": "(on disk, no DB record)",
"admin_files.orphan_files_heading": "Orphan files",
"admin_files.page_title": "File Manager Admin",
"admin_files.status_in_db": "In DB",
"admin_files.status_orphan": "Orphan",
"admin_files.tab_database": "Database Records",
"admin_files.tab_filesystem": "Filesystem",
"admin_files.tab_reconcile": "Reconcile",
"admin_plans.aria_delete_plan": "Delete {name}", "admin_plans.aria_delete_plan": "Delete {name}",
"admin_plans.aria_edit_plan": "Edit {name}", "admin_plans.aria_edit_plan": "Edit {name}",
"admin_plans.aria_feature_n": "Feature {n}", "admin_plans.aria_feature_n": "Feature {n}",
@@ -55,10 +102,10 @@
"admin_plans.btn_edit": "Edit", "admin_plans.btn_edit": "Edit",
"admin_plans.btn_restore_defaults": "Restore Defaults", "admin_plans.btn_restore_defaults": "Restore Defaults",
"admin_plans.btn_restore_defaults_title": "Restore all four default plans (only if no plans exist yet)", "admin_plans.btn_restore_defaults_title": "Restore all four default plans (only if no plans exist yet)",
"admin_plans.btn_restoring": "Restoring\u2026", "admin_plans.btn_restoring": "Restoring",
"admin_plans.btn_save_changes": "Save Changes", "admin_plans.btn_save_changes": "Save Changes",
"admin_plans.btn_save_order": "Save Order", "admin_plans.btn_save_order": "Save Order",
"admin_plans.btn_saving": "Saving\u2026", "admin_plans.btn_saving": "Saving",
"admin_plans.btn_stripe_setup": "Stripe Setup", "admin_plans.btn_stripe_setup": "Stripe Setup",
"admin_plans.btn_stripe_setup_title": "Open the Stripe Setup Wizard to configure API keys and sync plans", "admin_plans.btn_stripe_setup_title": "Open the Stripe Setup Wizard to configure API keys and sync plans",
"admin_plans.col_actions": "Actions", "admin_plans.col_actions": "Actions",
@@ -113,7 +160,7 @@
"admin_plans.js_seed_failed": "Seed failed", "admin_plans.js_seed_failed": "Seed failed",
"admin_plans.js_yearly_enter": "Enter yearly price to show savings", "admin_plans.js_yearly_enter": "Enter yearly price to show savings",
"admin_plans.js_yearly_save": "Save {pct}% vs monthly", "admin_plans.js_yearly_save": "Save {pct}% vs monthly",
"admin_plans.loading": "Loading plans\u2026", "admin_plans.loading": "Loading plans",
"admin_plans.modal_close_aria": "Close modal", "admin_plans.modal_close_aria": "Close modal",
"admin_plans.modal_create_title": "Add Plan", "admin_plans.modal_create_title": "Add Plan",
"admin_plans.modal_edit_title_prefix": "Edit Plan: ", "admin_plans.modal_edit_title_prefix": "Edit Plan: ",
@@ -122,17 +169,17 @@
"admin_plans.overage_0pct": "0% (exact)", "admin_plans.overage_0pct": "0% (exact)",
"admin_plans.overage_100pct": "100%", "admin_plans.overage_100pct": "100%",
"admin_plans.overage_50pct": "50%", "admin_plans.overage_50pct": "50%",
"admin_plans.overage_announce": "\u2192 announce", "admin_plans.overage_announce": " announce",
"admin_plans.overage_buffer_body_prefix": "The overage buffer is", "admin_plans.overage_buffer_body_prefix": "The overage buffer is",
"admin_plans.overage_buffer_body_suffix": "We advertise X docs/month but only enforce at", "admin_plans.overage_buffer_body_suffix": "We advertise X docs/month but only enforce at",
"admin_plans.overage_buffer_formula": "X \u00d7 (1 + buffer%)", "admin_plans.overage_buffer_formula": "X × (1 + buffer%)",
"admin_plans.overage_buffer_invisible": "invisible to users", "admin_plans.overage_buffer_invisible": "invisible to users",
"admin_plans.overage_buffer_tail": "docs. For example, a 150-doc/month plan with a 20% buffer enforces at 180 docs. This prevents hard cutoffs at the exact announced limit, giving users a graceful soft landing.", "admin_plans.overage_buffer_tail": "docs. For example, a 150-doc/month plan with a 20% buffer enforces at 180 docs. This prevents hard cutoffs at the exact announced limit, giving users a graceful soft landing.",
"admin_plans.overage_buffer_title": "About the Overage Buffer", "admin_plans.overage_buffer_title": "About the Overage Buffer",
"admin_plans.overage_docs": "docs,", "admin_plans.overage_docs": "docs,",
"admin_plans.overage_docs_end": "docs", "admin_plans.overage_docs_end": "docs",
"admin_plans.overage_enforce_at": "enforce at", "admin_plans.overage_enforce_at": "enforce at",
"admin_plans.page_title": "Plan Designer \u2014 DocuElevate Admin", "admin_plans.page_title": "Plan Designer DocuElevate Admin",
"admin_plans.section_basic_info": "Basic Info", "admin_plans.section_basic_info": "Basic Info",
"admin_plans.section_display": "Display", "admin_plans.section_display": "Display",
"admin_plans.section_features": "Features List", "admin_plans.section_features": "Features List",
@@ -149,53 +196,6 @@
"admin_plans.stripe_wizard_text": "Stripe Setup Wizard", "admin_plans.stripe_wizard_text": "Stripe Setup Wizard",
"admin_plans.subheading": "Manage subscription plans shown on the public pricing page.", "admin_plans.subheading": "Manage subscription plans shown on the public pricing page.",
"admin_plans.table_aria_label": "Subscription plans", "admin_plans.table_aria_label": "Subscription plans",
"admin_files.admin_only_badge": "Admin Only",
"admin_files.aria_breadcrumb": "Breadcrumb",
"admin_files.badge_delta_detected": "Delta detected",
"admin_files.badge_duplicate": "dup",
"admin_files.badge_in_db": "in DB",
"admin_files.badge_on_disk": "on disk",
"admin_files.breadcrumb_workdir": "workdir",
"admin_files.btn_download": "Download",
"admin_files.col_actions": "Actions",
"admin_files.col_db": "DB",
"admin_files.col_health": "Health",
"admin_files.col_id": "ID",
"admin_files.col_ingested": "Ingested",
"admin_files.col_local_filename": "local_filename",
"admin_files.col_missing_paths": "Missing paths",
"admin_files.col_modified": "Modified",
"admin_files.col_name": "Name",
"admin_files.col_original_file_path": "original_file_path",
"admin_files.col_original_filename": "Original Filename",
"admin_files.col_path_relative": "Path (relative to workdir)",
"admin_files.col_processed_file_path": "processed_file_path",
"admin_files.col_size": "Size",
"admin_files.delta_detected_detail": "Found {orphan_count} orphan file(s) on disk with no DB record, and {ghost_count} DB record(s) with missing files on disk.",
"admin_files.delta_detected_title": "Delta detected.",
"admin_files.empty_database": "No file records found in the database.",
"admin_files.empty_directory": "This directory is empty.",
"admin_files.ghost_records_desc": "(in DB, file(s) missing on disk)",
"admin_files.ghost_records_heading": "Ghost records",
"admin_files.heading": "File Manager",
"admin_files.health_missing": "Missing",
"admin_files.health_ok": "OK",
"admin_files.legend_file_exists": "File exists on disk",
"admin_files.legend_file_missing": "File missing from disk",
"admin_files.legend_found_in_db": "Found in DB",
"admin_files.legend_not_in_db": "Not in DB (orphan)",
"admin_files.legend_path_not_set": "Path not set",
"admin_files.no_delta": "No delta found \u2014 filesystem and database are in sync.",
"admin_files.no_ghost_records": "No ghost records found.",
"admin_files.no_orphan_files": "No orphan files found.",
"admin_files.orphan_files_desc": "(on disk, no DB record)",
"admin_files.orphan_files_heading": "Orphan files",
"admin_files.page_title": "File Manager \u2013 Admin",
"admin_files.status_in_db": "In DB",
"admin_files.status_orphan": "Orphan",
"admin_files.tab_database": "Database Records",
"admin_files.tab_filesystem": "Filesystem",
"admin_files.tab_reconcile": "Reconcile",
"admin_users.add_user_profile_btn": "Add User Profile", "admin_users.add_user_profile_btn": "Add User Profile",
"admin_users.admin_only_badge": "Admin Only", "admin_users.admin_only_badge": "Admin Only",
"admin_users.btn_password": "Password", "admin_users.btn_password": "Password",
@@ -505,6 +505,7 @@
"common.next_page": "Next page", "common.next_page": "Next page",
"common.no": "No", "common.no": "No",
"common.none": "None", "common.none": "None",
"common.not_available": "N/A",
"common.page": "Page", "common.page": "Page",
"common.paused": "Paused", "common.paused": "Paused",
"common.pending": "Pending", "common.pending": "Pending",
@@ -668,6 +669,7 @@
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more folders are processed recursively", "files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more folders are processed recursively",
"files.drop_overlay_title": "Drop files or folders anywhere to upload", "files.drop_overlay_title": "Drop files or folders anywhere to upload",
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.", "files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
"files.file_actions_aria": "File actions",
"files.file_size": "File Size", "files.file_size": "File Size",
"files.filename": "Filename", "files.filename": "Filename",
"files.files_selected": "files selected", "files.files_selected": "files selected",
@@ -695,6 +697,50 @@
"files.filter_tags_placeholder": "e.g. invoice,amazon", "files.filter_tags_placeholder": "e.g. invoice,amazon",
"files.fulltext_search_label": "Full-Text Search (OCR text, metadata, tags)", "files.fulltext_search_label": "Full-Text Search (OCR text, metadata, tags)",
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...", "files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
"files.js_confirm_cloud_ocr": "Are you sure you want to re-run Cloud OCR on {count} file(s)?",
"files.js_confirm_delete_files": "Are you sure you want to delete {count} file(s)?",
"files.js_confirm_reprocess_files": "Are you sure you want to reprocess {count} file(s)?",
"files.js_delete_saved_search_aria": "Delete saved search {name}",
"files.js_delete_saved_search_confirm": "Delete this saved search?",
"files.js_download": "Download",
"files.js_download_file_aria": "Download file",
"files.js_enter_search_name": "Enter a name for this saved search:",
"files.js_error_deleting_file": "Error deleting file: {error}",
"files.js_error_deleting_files": "Error deleting files: {error}",
"files.js_error_downloading_files": "Error downloading files: {error}",
"files.js_error_queuing_cloud_ocr": "Error queuing Cloud OCR: {error}",
"files.js_error_reprocessing_files": "Error reprocessing files: {error}",
"files.js_failed_create_zip": "Failed to create ZIP download",
"files.js_failed_delete_file": "Failed to delete file",
"files.js_failed_delete_files": "Failed to delete files",
"files.js_failed_load_file": "Failed to load file",
"files.js_failed_load_pdf": "Failed to load PDF",
"files.js_failed_queue_cloud_ocr": "Failed to queue Cloud OCR",
"files.js_failed_reprocess_files": "Failed to reprocess files",
"files.js_full_view": "Full view",
"files.js_load_saved_searches_error": "Could not load saved searches",
"files.js_loading": "Loading…",
"files.js_next_page": "Next »",
"files.js_no_filters_to_save": "No filters to save. Please apply at least one filter (search, date, status, tags, etc.) before saving.",
"files.js_no_results": "No results found.",
"files.js_no_saved_searches": "No saved searches yet",
"files.js_open_full_view_aria": "Open full view",
"files.js_page_info": "Page {page} / {pages}",
"files.js_pdf_page_info": "Page {page} of {total}",
"files.js_prev_page": "« Prev",
"files.js_preview_fallback": "Preview",
"files.js_reset_zoom": "Reset zoom",
"files.js_search_results_count": "{total} result(s) for \"{query}\"",
"files.js_search_unavailable": "Search unavailable: {error}",
"files.js_searching": "Searching…",
"files.js_select_files_cloud_ocr": "Please select files to re-run Cloud OCR on",
"files.js_select_files_delete": "Please select files to delete",
"files.js_select_files_download": "Please select files to download",
"files.js_select_files_reprocess": "Please select files to reprocess",
"files.js_untitled": "(untitled)",
"files.js_view_file": "View file",
"files.js_zoom_in": "Zoom in",
"files.js_zoom_out": "Zoom out",
"files.no_files": "No files found", "files.no_files": "No files found",
"files.ocr_status": "OCR Status", "files.ocr_status": "OCR Status",
"files.page_title": "File Records", "files.page_title": "File Records",
@@ -1707,6 +1753,21 @@
"terms.s6_p3_mid": ". For licensing information, please refer to our", "terms.s6_p3_mid": ". For licensing information, please refer to our",
"terms.s6_p3_post": ".", "terms.s6_p3_post": ".",
"terms.s6_p3_pre": "For information about how we use cookies, please see our", "terms.s6_p3_pre": "For information about how we use cookies, please see our",
"translation.copied": "Copied!",
"translation.copy": "Copy",
"translation.default_language_version": "Default Language Version",
"translation.detected_language": "Detected language",
"translation.hide_text": "Hide text",
"translation.load_translation": "Load translation",
"translation.no_translation": "No translation available yet — it may still be processing",
"translation.select_language": "Select language…",
"translation.select_target": "Please select a target language.",
"translation.show_text": "Show text",
"translation.translate_btn": "Translate",
"translation.translate_to": "Translate to Another Language",
"translation.translated_to": "Translated to",
"translation.translating": "Translating…",
"translation.translation_failed": "Translation failed",
"upload.browse_button": "Browse Files", "upload.browse_button": "Browse Files",
"upload.button_processing": "Processing...", "upload.button_processing": "Processing...",
"upload.camera_button": "Take Photo / Scan Document", "upload.camera_button": "Take Photo / Scan Document",
@@ -1734,21 +1795,5 @@
"upload.uploading": "Uploading...", "upload.uploading": "Uploading...",
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)", "upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
"upload.url_label": "File URL", "upload.url_label": "File URL",
"upload.url_placeholder": "https://example.com/document.pdf", "upload.url_placeholder": "https://example.com/document.pdf"
"translation.default_language_version": "Default Language Version",
"translation.detected_language": "Detected language",
"translation.show_text": "Show text",
"translation.hide_text": "Hide text",
"translation.copy": "Copy",
"translation.load_translation": "Load translation",
"translation.translate_to": "Translate to Another Language",
"translation.select_language": "Select language…",
"translation.translate_btn": "Translate",
"translation.translating": "Translating…",
"translation.no_translation": "No translation available yet — it may still be processing",
"translation.translated_to": "Translated to",
"translation.translation_failed": "Translation failed",
"translation.select_target": "Please select a target language.",
"translation.copied": "Copied!"
} }