feat(i18n): replace hardcoded English text with translation keys in files, settings, and pipelines templates

Replace all hardcoded English strings in three HTML Jinja2 templates with
`{{ _("key") }}` translation function calls, adding 87 new keys to en.json.

files.html:
- Page title, drop overlay, upload modal, error messages
- All filter labels, placeholders, options (status, OCR quality, etc.)
- Saved searches and full-text search UI
- Bulk action buttons, table headers, pagination
- Delete and preview modal text

settings.html:
- Page heading, configuration priority legend
- Header buttons (Wizard, DB Wizard, Export, Audit Log)
- Search bar placeholder and aria-labels
- Sidebar categories, no-results state
- Per-setting labels: required, enable prefix, effective value
- Autocomplete and model picker hints/placeholders
- Revert/save button labels and states

pipelines.html:
- Page title and subtitle
- New Pipeline button, loading state, empty state
- Pipeline card badges (System, Default, Inactive, Disabled)
- Add/Edit step modal fields and labels
- OCR language options and hints
- Cancel/Delete buttons in all modals

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-12 00:17:23 +00:00
parent bea9dae957
commit 8c77982109
4 changed files with 247 additions and 168 deletions
+78 -79
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}File Records{% endblock %}
{% block title %}{{ _("files.page_title") }}{% endblock %}
{% block head_extra %}
<script src="/static/js/common.js"></script>
@@ -427,16 +427,16 @@
<div id="dropOverlay" class="drop-overlay" aria-hidden="true">
<div class="drop-message">
<i class="fas fa-cloud-upload-alt" aria-hidden="true"></i>
<p>Drop files or folders anywhere to upload</p>
<div class="drop-hint">Supports PDF, Office docs, images, HTML, Markdown, and more folders are processed recursively</div>
<p>{{ _("files.drop_overlay_title") }}</p>
<div class="drop-hint">{{ _("files.drop_overlay_hint") }}</div>
</div>
</div>
<!-- Upload progress modal -->
<div id="uploadModal" class="upload-modal" role="dialog" aria-modal="true" aria-label="Upload progress">
<div id="uploadModal" class="upload-modal" role="dialog" aria-modal="true" aria-label="{{ _('files.upload_modal_aria') }}">
<div class="upload-modal-header">
<span><i class="fas fa-upload" aria-hidden="true"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()" aria-label="Close upload progress">
<span><i class="fas fa-upload" aria-hidden="true"></i> {{ _("files.upload_modal_header") }}</span>
<button class="close-modal-btn" onclick="closeUploadModal()" aria-label="{{ _('files.upload_modal_close') }}">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
@@ -447,7 +447,7 @@
</div>
<div class="container mx-auto px-4 py-8">
<h2 class="text-3xl font-bold mb-6">File Records</h2>
<h2 class="text-3xl font-bold mb-6">{{ _("files.page_title") }}</h2>
<!-- Queue pending banner (populated via JS) -->
<div id="queueBanner" class="hidden bg-blue-50 border-l-4 border-blue-400 text-blue-800 p-4 mb-6 rounded-r-lg" role="status">
@@ -455,36 +455,35 @@
<div class="flex items-center">
<i class="fas fa-spinner fa-spin mr-3 text-blue-500" aria-hidden="true"></i>
<span>
<strong id="queueBannerCount">0</strong> item(s) are currently queued or being processed.
Files will appear here once processing completes.
<strong id="queueBannerCount">0</strong> {{ _("files.queue_banner_items") }}
</span>
</div>
<a href="/admin/queue" class="text-blue-600 hover:text-blue-800 text-sm font-medium whitespace-nowrap ml-4"
id="queueBannerLink" style="display:none;">
<i class="fas fa-external-link-alt mr-1" aria-hidden="true"></i>View Queue
<i class="fas fa-external-link-alt mr-1" aria-hidden="true"></i>{{ _("files.queue_banner_link") }}
</a>
</div>
</div>
{% if error %}
<div class="error-message">
<p><strong>Error:</strong> {{ error }}</p>
<p class="mt-2">This might be due to a configuration or import issue. Please check the server logs.</p>
<p><strong>{{ _("common.error") }}:</strong> {{ error }}</p>
<p class="mt-2">{{ _("files.error_hint") }}</p>
</div>
{% endif %}
<!-- Filters Section -->
<div class="filters-section">
<form method="get" action="/files" class="filter-group" role="search" aria-label="Filter files">
<form method="get" action="/files" class="filter-group" role="search" aria-label="{{ _('files.filter_form_aria') }}">
<div class="filter-item">
<label for="search">Search Filename</label>
<input type="text" id="search" name="search" value="{{ search }}" placeholder="Enter filename...">
<label for="search">{{ _("files.filter_search_label") }}</label>
<input type="text" id="search" name="search" value="{{ search }}" placeholder="{{ _('files.filter_search_placeholder') }}">
</div>
<div class="filter-item">
<label for="mime_type">MIME Type</label>
<label for="mime_type">{{ _("files.filter_mime_type") }}</label>
<select id="mime_type" name="mime_type">
<option value="">All Types</option>
<option value="">{{ _("files.filter_all_types") }}</option>
{% for mt in mime_types %}
<option value="{{ mt }}" {% if mt == mime_type %}selected{% endif %}>{{ mt }}</option>
{% endfor %}
@@ -492,31 +491,31 @@
</div>
<div class="filter-item">
<label for="status">Status</label>
<label for="status">{{ _("common.status") }}</label>
<select id="status" name="status">
<option value="">All Statuses</option>
<option value="pending" {% if status == "pending" %}selected{% endif %}>Pending</option>
<option value="processing" {% if status == "processing" %}selected{% endif %}>Processing</option>
<option value="completed" {% if status == "completed" %}selected{% endif %}>Completed</option>
<option value="failed" {% if status == "failed" %}selected{% endif %}>Failed</option>
<option value="duplicate" {% if status == "duplicate" %}selected{% endif %}>Duplicate</option>
<option value="">{{ _("files.filter_all_statuses") }}</option>
<option value="pending" {% if status == "pending" %}selected{% endif %}>{{ _("common.pending") }}</option>
<option value="processing" {% if status == "processing" %}selected{% endif %}>{{ _("common.processing") }}</option>
<option value="completed" {% if status == "completed" %}selected{% endif %}>{{ _("common.completed") }}</option>
<option value="failed" {% if status == "failed" %}selected{% endif %}>{{ _("common.failed") }}</option>
<option value="duplicate" {% if status == "duplicate" %}selected{% endif %}>{{ _("files.status_duplicate") }}</option>
</select>
</div>
<div class="filter-item">
<label for="date_from">Date From</label>
<input type="date" id="date_from" name="date_from" value="{{ date_from }}" aria-label="Filter from date">
<label for="date_from">{{ _("files.filter_date_from") }}</label>
<input type="date" id="date_from" name="date_from" value="{{ date_from }}" aria-label="{{ _('files.filter_date_from_aria') }}">
</div>
<div class="filter-item">
<label for="date_to">Date To</label>
<input type="date" id="date_to" name="date_to" value="{{ date_to }}" aria-label="Filter to date">
<label for="date_to">{{ _("files.filter_date_to") }}</label>
<input type="date" id="date_to" name="date_to" value="{{ date_to }}" aria-label="{{ _('files.filter_date_to_aria') }}">
</div>
<div class="filter-item">
<label for="storage_provider">Storage Provider</label>
<label for="storage_provider">{{ _("files.filter_storage_provider") }}</label>
<select id="storage_provider" name="storage_provider">
<option value="">All Providers</option>
<option value="">{{ _("files.filter_all_providers") }}</option>
<option value="dropbox" {% if storage_provider == "dropbox" %}selected{% endif %}>Dropbox</option>
<option value="google_drive" {% if storage_provider == "google_drive" %}selected{% endif %}>Google Drive</option>
<option value="onedrive" {% if storage_provider == "onedrive" %}selected{% endif %}>OneDrive</option>
@@ -529,28 +528,28 @@
</div>
<div class="filter-item">
<label for="tags">Tags</label>
<input type="text" id="tags" name="tags" value="{{ tags }}" placeholder="e.g. invoice,amazon" aria-label="Filter by tags (comma-separated)">
<label for="tags">{{ _("files.tags") }}</label>
<input type="text" id="tags" name="tags" value="{{ tags }}" placeholder="{{ _('files.filter_tags_placeholder') }}" aria-label="{{ _('files.filter_tags_aria') }}">
</div>
<div class="filter-item">
<label for="ocr_quality">OCR Quality</label>
<select id="ocr_quality" name="ocr_quality" aria-label="Filter by OCR quality score">
<option value="">All Files</option>
<option value="poor" {% if ocr_quality == "poor" %}selected{% endif %} aria-label="Poor quality, score less than {{ ocr_quality_threshold }}">Poor (score &lt; {{ ocr_quality_threshold }})</option>
<option value="good" {% if ocr_quality == "good" %}selected{% endif %} aria-label="Good quality, score at least {{ ocr_quality_threshold }}">Good (score &ge; {{ ocr_quality_threshold }})</option>
<option value="unchecked" {% if ocr_quality == "unchecked" %}selected{% endif %} aria-label="Not yet assessed">Not yet assessed</option>
<label for="ocr_quality">{{ _("files.filter_ocr_quality") }}</label>
<select id="ocr_quality" name="ocr_quality" aria-label="{{ _('files.filter_ocr_quality_aria') }}">
<option value="">{{ _("files.filter_ocr_all") }}</option>
<option value="poor" {% if ocr_quality == "poor" %}selected{% endif %} aria-label="Poor quality, score less than {{ ocr_quality_threshold }}">{{ _("files.filter_ocr_poor") }} (score &lt; {{ ocr_quality_threshold }})</option>
<option value="good" {% if ocr_quality == "good" %}selected{% endif %} aria-label="Good quality, score at least {{ ocr_quality_threshold }}">{{ _("files.filter_ocr_good") }} (score &ge; {{ ocr_quality_threshold }})</option>
<option value="unchecked" {% if ocr_quality == "unchecked" %}selected{% endif %} aria-label="Not yet assessed">{{ _("files.filter_ocr_unchecked") }}</option>
</select>
</div>
<div class="filter-item">
<label>&nbsp;</label>
<button type="submit">Apply Filters</button>
<button type="submit">{{ _("files.filter_apply") }}</button>
</div>
<div class="filter-item">
<label>&nbsp;</label>
<button type="button" class="clear" onclick="clearFilters()">Clear</button>
<button type="button" class="clear" onclick="clearFilters()">{{ _("files.filter_clear") }}</button>
</div>
<!-- Hidden fields to preserve sort order -->
@@ -564,13 +563,13 @@
<div class="filters-section" style="margin-top: 0.5rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; width: 100%;">
<label style="font-weight: 600; font-size: 0.85rem; white-space: nowrap;">
<i class="fas fa-bookmark" aria-hidden="true"></i> Saved Searches:
<i class="fas fa-bookmark" aria-hidden="true"></i> {{ _("files.saved_searches_label") }}
</label>
<div id="saved-searches-list" style="display: flex; gap: 0.25rem; flex-wrap: wrap;" aria-live="polite">
<span style="color: var(--text-muted); font-size: 0.85rem;">Loading...</span>
<span style="color: var(--text-muted); font-size: 0.85rem;">{{ _("common.loading") }}</span>
</div>
<button type="button" onclick="saveCurrentFilters()" class="btn-save-search" style="margin-left: auto; font-size: 0.8rem; padding: 0.25rem 0.5rem; cursor: pointer;" aria-label="Save current filters as a saved search">
<i class="fas fa-plus" aria-hidden="true"></i> Save Current
<button type="button" onclick="saveCurrentFilters()" class="btn-save-search" style="margin-left: auto; font-size: 0.8rem; padding: 0.25rem 0.5rem; cursor: pointer;" aria-label="{{ _('files.saved_searches_save_aria') }}">
<i class="fas fa-plus" aria-hidden="true"></i> {{ _("files.saved_searches_save") }}
</button>
</div>
</div>
@@ -579,13 +578,13 @@
<div class="filters-section" style="margin-top: 0.75rem;">
<div style="width: 100%;">
<label for="fulltext-search" style="font-weight: 600; display: block; margin-bottom: 0.4rem;">
<i class="fas fa-search" aria-hidden="true"></i> Full-Text Search (OCR text, metadata, tags)
<i class="fas fa-search" aria-hidden="true"></i> {{ _("files.fulltext_search_label") }}
</label>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input
type="text"
id="fulltext-search"
placeholder="Search document content, sender, tags, type..."
placeholder="{{ _('files.fulltext_search_placeholder') }}"
style="flex: 1; min-width: 220px; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.875rem;"
oninput="debounceSearch(this.value)"
>
@@ -594,14 +593,14 @@
onclick="runFullTextSearch()"
style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; white-space: nowrap;"
>
Search
{{ _("common.search") }}
</button>
<button
type="button"
onclick="clearFullTextSearch()"
style="padding: 0.5rem 1rem; background-color: #6b7280; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; white-space: nowrap;"
>
Clear
{{ _("files.filter_clear") }}
</button>
</div>
</div>
@@ -620,23 +619,23 @@
<div id="bulkActionsBar" class="filters-section" style="display: none; background-color: #e6f3ff;">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3">
<div>
<strong id="selectedCount">0</strong> files selected
<strong id="selectedCount">0</strong> {{ _("files.files_selected") }}
</div>
<div class="flex flex-wrap gap-2">
<button type="button" onclick="bulkReprocess()" style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-sync" aria-hidden="true"></i> Reprocess Selected
<i class="fas fa-sync" aria-hidden="true"></i> {{ _("files.bulk_reprocess") }}
</button>
<button type="button" onclick="bulkReprocessCloudOcr()" style="padding: 0.5rem 1rem; background-color: #805ad5; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-cloud" aria-hidden="true"></i> Re-run Cloud OCR
<i class="fas fa-cloud" aria-hidden="true"></i> {{ _("files.bulk_cloud_ocr") }}
</button>
<button type="button" onclick="bulkDownload()" style="padding: 0.5rem 1rem; background-color: #38a169; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-file-archive" aria-hidden="true"></i> Download as ZIP
<i class="fas fa-file-archive" aria-hidden="true"></i> {{ _("files.bulk_download") }}
</button>
<button type="button" onclick="bulkDelete()" style="padding: 0.5rem 1rem; background-color: #e53e3e; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-trash" aria-hidden="true"></i> Delete Selected
<i class="fas fa-trash" aria-hidden="true"></i> {{ _("files.bulk_delete") }}
</button>
<button type="button" onclick="clearSelection()" style="padding: 0.5rem 1rem; background-color: #718096; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
Clear Selection
{{ _("files.bulk_clear_selection") }}
</button>
</div>
</div>
@@ -644,14 +643,14 @@
<!-- File table (desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="file-table" id="fileTable" aria-label="File records">
<table class="file-table" id="fileTable" aria-label="{{ _('files.table_aria') }}">
<thead>
<tr>
<th scope="col" style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page" aria-label="Select all files on this page">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="{{ _('files.table_select_all') }}" aria-label="{{ _('files.table_select_all') }}">
</th>
<th scope="col" class="sortable" onclick="sortTable('id')" role="columnheader" aria-sort="{% if sort_by == 'id' %}{% if sort_order == 'asc' %}ascending{% else %}descending{% endif %}{% else %}none{% endif %}">
ID
{{ _("files.table_id") }}
<span class="sort-indicator {% if sort_by == 'id' %}active{% endif %}">
{% if sort_by == 'id' %}
{% if sort_order == 'asc' %}▲{% else %}▼{% endif %}
@@ -661,7 +660,7 @@
</span>
</th>
<th scope="col" class="sortable" onclick="sortTable('original_filename')">
Original Filename
{{ _("files.table_original_filename") }}
<span class="sort-indicator {% if sort_by == 'original_filename' %}active{% endif %}">
{% if sort_by == 'original_filename' %}
{% if sort_order == 'asc' %}▲{% else %}▼{% endif %}
@@ -671,7 +670,7 @@
</span>
</th>
<th scope="col" class="sortable" onclick="sortTable('file_size')">
File Size
{{ _("files.file_size") }}
<span class="sort-indicator {% if sort_by == 'file_size' %}active{% endif %}">
{% if sort_by == 'file_size' %}
{% if sort_order == 'asc' %}▲{% else %}▼{% endif %}
@@ -681,7 +680,7 @@
</span>
</th>
<th scope="col" class="sortable" onclick="sortTable('mime_type')">
MIME Type
{{ _("files.table_mime_type") }}
<span class="sort-indicator {% if sort_by == 'mime_type' %}active{% endif %}">
{% if sort_by == 'mime_type' %}
{% if sort_order == 'asc' %}▲{% else %}▼{% endif %}
@@ -690,9 +689,9 @@
{% endif %}
</span>
</th>
<th scope="col">Status</th>
<th scope="col">{{ _("common.status") }}</th>
<th scope="col" class="sortable" onclick="sortTable('created_at')">
Created At
{{ _("files.table_created_at") }}
<span class="sort-indicator {% if sort_by == 'created_at' %}active{% endif %}">
{% if sort_by == 'created_at' %}
{% if sort_order == 'asc' %}▲{% else %}▼{% endif %}
@@ -701,7 +700,7 @@
{% endif %}
</span>
</th>
<th scope="col">Actions</th>
<th scope="col">{{ _("files.table_actions") }}</th>
</tr>
</thead>
<tbody>
@@ -736,7 +735,7 @@
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center py-4">No files found</td>
<td colspan="8" class="text-center py-4">{{ _("files.table_empty") }}</td>
</tr>
{% endfor %}
</tbody>
@@ -778,33 +777,33 @@
</div>
</div>
{% else %}
<p class="text-center py-4 text-gray-500">No files found</p>
<p class="text-center py-4 text-gray-500">{{ _("files.table_empty") }}</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination.pages > 1 %}
<nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<nav class="pagination flex-wrap gap-2" aria-label="{{ _('files.pagination_nav_aria') }}">
<div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ _("files.pagination_showing") }} {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total) }}
of {{ pagination.total }} files
{{ _("files.pagination_of") }} {{ pagination.total }} {{ _("files.pagination_files") }}
</div>
<div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %}
<button class="pagination-button" onclick="goToPage(1)" aria-label="Go to first page" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button>
<button class="pagination-button" onclick="goToPage(1)" aria-label="{{ _('files.pagination_first') }}" style="min-height:44px;">{{ _("files.pagination_first") }}</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="{{ _('files.pagination_previous') }}" style="min-height:44px;">{{ _("files.pagination_previous") }}</button>
{% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="Page {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;">
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="{{ _('common.page') }} {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;">
{{ p }}
</button>
{% endfor %}
{% if pagination.page < pagination.pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="Go to next page" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="{{ _('files.pagination_next') }}" style="min-height:44px;">{{ _("files.pagination_next") }}</button>
<button class="pagination-button" onclick="goToPage({{ pagination.pages }})" aria-label="{{ _('files.pagination_last') }}" style="min-height:44px;">{{ _("files.pagination_last") }}</button>
{% endif %}
</div>
</nav>
@@ -813,11 +812,11 @@
<!-- Delete confirmation modal -->
<div id="deleteModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="deleteModalTitle">
<div class="modal-content">
<div class="modal-title" id="deleteModalTitle">Confirm Deletion</div>
<p>Are you sure you want to delete this file?</p>
<div class="modal-title" id="deleteModalTitle">{{ _("files.delete_modal_title") }}</div>
<p>{{ _("files.delete_modal_message") }}</p>
<div class="modal-buttons">
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">Cancel</button>
<button id="confirmDelete" class="modal-btn modal-btn-delete" style="min-height:44px;min-width:80px;">Delete</button>
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">{{ _("files.delete_modal_cancel") }}</button>
<button id="confirmDelete" class="modal-btn modal-btn-delete" style="min-height:44px;min-width:80px;">{{ _("files.delete_modal_confirm") }}</button>
</div>
</div>
</div>
@@ -826,8 +825,8 @@
<div id="previewOverlay" class="preview-overlay" role="dialog" aria-modal="true" aria-labelledby="previewTitle">
<div class="preview-panel">
<div class="preview-panel-header">
<h3 id="previewTitle">Preview</h3>
<button class="preview-panel-close" onclick="closePreviewModal()" aria-label="Close preview">
<h3 id="previewTitle">{{ _("files.preview_modal_title") }}</h3>
<button class="preview-panel-close" onclick="closePreviewModal()" aria-label="{{ _('files.preview_modal_close') }}">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>