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:
@@ -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 < {{ 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 ≥ {{ 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 < {{ 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 ≥ {{ 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> </label>
|
||||
<button type="submit">Apply Filters</button>
|
||||
<button type="submit">{{ _("files.filter_apply") }}</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label> </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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Processing Pipelines – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("pipelines.title") }} – DocuElevate{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8" x-data="pipelinesApp()" x-init="init()">
|
||||
@@ -9,13 +9,13 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<i class="fas fa-project-diagram text-blue-500" aria-hidden="true"></i>
|
||||
Processing Pipelines
|
||||
{{ _("pipelines.page_title") }}
|
||||
</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">
|
||||
Define and manage custom document processing workflows.
|
||||
System pipelines (created by admins) are shown with a
|
||||
<span class="text-xs font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1">System</span>
|
||||
badge and are visible to all users.
|
||||
{{ _("pipelines.subtitle_intro") }}
|
||||
{{ _("pipelines.subtitle_system_pre") }}
|
||||
<span class="text-xs font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1">{{ _("pipelines.system_label") }}</span>
|
||||
{{ _("pipelines.subtitle_system_post") }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -24,7 +24,7 @@
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i> New Pipeline
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i> {{ _("pipelines.new_pipeline_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<template x-if="loading">
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin text-3xl mb-3" aria-hidden="true"></i>
|
||||
<p>Loading pipelines…</p>
|
||||
<p>{{ _("pipelines.loading") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,15 +59,15 @@
|
||||
<template x-if="!loading && pipelines.length === 0">
|
||||
<div class="text-center py-16 bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<i class="fas fa-project-diagram text-5xl text-gray-300 mb-4" aria-hidden="true"></i>
|
||||
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">No pipelines yet</h2>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-6">Create your first pipeline to define custom document processing workflows.</p>
|
||||
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">{{ _("pipelines.empty_state") }}</h2>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-6">{{ _("pipelines.empty_state_hint") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreatePipelineModal()"
|
||||
class="inline-flex items-center px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i> Create Pipeline
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i> {{ _("pipelines.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -84,13 +84,13 @@
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white" x-text="pipeline.name"></h2>
|
||||
<!-- Badges -->
|
||||
<template x-if="pipeline.owner_id === null">
|
||||
<span class="text-xs font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1.5 py-0.5">System</span>
|
||||
<span class="text-xs font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1.5 py-0.5">{{ _("pipelines.system_label") }}</span>
|
||||
</template>
|
||||
<template x-if="pipeline.is_default">
|
||||
<span class="text-xs font-semibold text-green-700 bg-green-50 border border-green-200 rounded px-1.5 py-0.5">Default</span>
|
||||
<span class="text-xs font-semibold text-green-700 bg-green-50 border border-green-200 rounded px-1.5 py-0.5">{{ _("pipelines.default_label") }}</span>
|
||||
</template>
|
||||
<template x-if="!pipeline.is_active">
|
||||
<span class="text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded px-1.5 py-0.5">Inactive</span>
|
||||
<span class="text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded px-1.5 py-0.5">{{ _("pipelines.inactive_label") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
style="min-height:36px;"
|
||||
:aria-label="`Edit pipeline ${pipeline.name}`"
|
||||
>
|
||||
<i class="fas fa-pencil-alt mr-1" aria-hidden="true"></i> Edit
|
||||
<i class="fas fa-pencil-alt mr-1" aria-hidden="true"></i> {{ _("common.edit") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -124,7 +124,7 @@
|
||||
style="min-height:36px;"
|
||||
:aria-label="`Delete pipeline ${pipeline.name}`"
|
||||
>
|
||||
<i class="fas fa-trash mr-1" aria-hidden="true"></i> Delete
|
||||
<i class="fas fa-trash mr-1" aria-hidden="true"></i> {{ _("common.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
<!-- Step list -->
|
||||
<template x-if="(pipeline.steps || []).length === 0">
|
||||
<p class="text-sm text-gray-400 italic mb-3">No steps defined. Add a step to start building your pipeline.</p>
|
||||
<p class="text-sm text-gray-400 italic mb-3">{{ _("pipelines.no_steps") }}</p>
|
||||
</template>
|
||||
|
||||
<template x-if="(pipeline.steps || []).length > 0">
|
||||
@@ -153,7 +153,7 @@
|
||||
<p class="text-xs text-gray-400 truncate" x-text="step.step_type"></p>
|
||||
</div>
|
||||
<template x-if="!step.enabled">
|
||||
<span class="text-xs text-gray-400 bg-gray-200 rounded px-1.5 py-0.5 flex-shrink-0">Disabled</span>
|
||||
<span class="text-xs text-gray-400 bg-gray-200 rounded px-1.5 py-0.5 flex-shrink-0">{{ _("pipelines.disabled_label") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
@@ -201,7 +201,7 @@
|
||||
style="min-height:36px;"
|
||||
:aria-label="`Add step to ${pipeline.name}`"
|
||||
>
|
||||
<i class="fas fa-plus mr-1" aria-hidden="true"></i> Add Step
|
||||
<i class="fas fa-plus mr-1" aria-hidden="true"></i> {{ _("pipelines.add_step_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="pipelineName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name <span aria-hidden="true" class="text-red-500">*</span>
|
||||
{{ _("common.name") }} <span aria-hidden="true" class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="pipelineName"
|
||||
@@ -250,21 +250,21 @@
|
||||
maxlength="255"
|
||||
required
|
||||
class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="My pipeline"
|
||||
placeholder="{{ _('pipelines.name_placeholder') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="pipelineDesc" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<label for="pipelineDesc" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _("common.description") }}</label>
|
||||
<textarea
|
||||
id="pipelineDesc"
|
||||
x-model="pipelineModal.form.description"
|
||||
rows="2"
|
||||
maxlength="4096"
|
||||
class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-700 dark:text-white resize-none"
|
||||
placeholder="Optional description"
|
||||
placeholder="{{ _('pipelines.description_placeholder') }}"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
x-model="pipelineModal.form.is_default"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Set as my default pipeline</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _("pipelines.set_default") }}</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@@ -284,7 +284,7 @@
|
||||
x-model="pipelineModal.form.is_active"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Active</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _("pipelines.active_label") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
x-model="pipelineModal.form.system"
|
||||
class="rounded border-gray-300 text-purple-600 focus:ring-purple-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">System pipeline (visible to all users)</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _("pipelines.system_pipeline_label") }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -310,14 +310,13 @@
|
||||
@click="closePipelineModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50"
|
||||
style="min-height:44px;"
|
||||
>Cancel</button>
|
||||
>{{ _("common.cancel") }}</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="pipelineModal.saving"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-60 rounded-md"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<template x-if="pipelineModal.saving">
|
||||
<i class="fas fa-spinner fa-spin mr-1" aria-hidden="true"></i>
|
||||
</template>
|
||||
<span x-text="pipelineModal.editMode ? 'Save Changes' : 'Create'"></span>
|
||||
@@ -358,7 +357,7 @@
|
||||
<!-- Step type -->
|
||||
<div>
|
||||
<label for="stepType" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Step Type <span aria-hidden="true" class="text-red-500">*</span>
|
||||
{{ _("pipelines.step_type_label") }} <span aria-hidden="true" class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="stepType"
|
||||
@@ -366,7 +365,7 @@
|
||||
:disabled="stepModal.editMode"
|
||||
class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="">— select —</option>
|
||||
<option value="">{{ _("common.select_placeholder") }}</option>
|
||||
<template x-for="[key, meta] in Object.entries(stepTypes)" :key="key">
|
||||
<option :value="key" x-text="meta.label"></option>
|
||||
</template>
|
||||
@@ -379,7 +378,7 @@
|
||||
<!-- Custom label -->
|
||||
<div>
|
||||
<label for="stepLabel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Custom Label <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("pipelines.custom_label_label") }} <span class="text-gray-400 font-normal">{{ _("pipelines.optional_suffix") }}</span>
|
||||
</label>
|
||||
<input
|
||||
id="stepLabel"
|
||||
@@ -387,7 +386,7 @@
|
||||
x-model="stepModal.form.label"
|
||||
maxlength="255"
|
||||
class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Override the default step name"
|
||||
placeholder="{{ _('pipelines.step_label_placeholder') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
@@ -400,7 +399,7 @@
|
||||
x-model="stepModal.form.config.force_cloud_ocr"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Force cloud OCR (skip local text extraction)</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _("pipelines.force_cloud_ocr_label") }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -408,14 +407,14 @@
|
||||
<template x-if="stepModal.form.step_type === 'ocr'">
|
||||
<div>
|
||||
<label for="ocrLanguage" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
OCR Language
|
||||
{{ _("pipelines.ocr_language_label") }}
|
||||
</label>
|
||||
<select
|
||||
id="ocrLanguage"
|
||||
x-model="stepModal.form.config.ocr_language"
|
||||
class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="auto">Auto (use system default)</option>
|
||||
<option value="auto">{{ _("pipelines.ocr_auto") }}</option>
|
||||
<option value="ara">Arabic</option>
|
||||
<option value="chi_sim">Chinese (Simplified)</option>
|
||||
<option value="chi_tra">Chinese (Traditional)</option>
|
||||
@@ -446,7 +445,7 @@
|
||||
<option value="vie">Vietnamese</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-400">
|
||||
Overrides the global language setting for Tesseract/EasyOCR. Azure and Mistral auto-detect the language.
|
||||
{{ _("pipelines.ocr_language_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -458,7 +457,7 @@
|
||||
x-model="stepModal.form.enabled"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Enabled</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _("pipelines.enabled_label") }}</span>
|
||||
</label>
|
||||
|
||||
<!-- Error -->
|
||||
@@ -471,7 +470,7 @@
|
||||
@click="closeStepModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50"
|
||||
style="min-height:44px;"
|
||||
>Cancel</button>
|
||||
>{{ _("common.cancel") }}</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="stepModal.saving"
|
||||
@@ -515,13 +514,13 @@
|
||||
@click="confirmModal.open = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50"
|
||||
style="min-height:44px;"
|
||||
>Cancel</button>
|
||||
>{{ _("common.cancel") }}</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="confirmModal.action(); confirmModal.open = false"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md"
|
||||
style="min-height:44px;"
|
||||
>Delete</button>
|
||||
>{{ _("common.delete") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("settings.title") }} - DocuElevate{% endblock %}
|
||||
|
||||
{# Category icon mapping #}
|
||||
{% set category_icons = {
|
||||
@@ -48,27 +48,27 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start gap-4 mb-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">Application Settings</h1>
|
||||
<h1 class="text-3xl font-bold mb-1">{{ _("settings.page_heading") }}</h1>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Manage configuration. Priority: <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">DB</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 ml-1">ENV</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 ml-1">DEFAULT</span>
|
||||
<span class="mx-1.5">·</span> <span class="text-red-600">*</span> = restart required <span class="mx-1.5">·</span> <i class="fas fa-lock text-xs" aria-hidden="true"></i> = encrypted at rest
|
||||
{{ _("settings.manage_config_prefix") }} <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">{{ _("settings.source_db_badge") }}</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 ml-1">{{ _("settings.source_env_badge") }}</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 ml-1">{{ _("settings.source_default_badge") }}</span>
|
||||
<span class="mx-1.5">·</span> <span class="text-red-600">*</span> {{ _("settings.restart_required_suffix") }} <span class="mx-1.5">·</span> <i class="fas fa-lock text-xs" aria-hidden="true"></i> {{ _("settings.encrypted_suffix") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<a href="/setup?step=1"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-magic mr-1.5" aria-hidden="true"></i> Wizard
|
||||
<i class="fas fa-magic mr-1.5" aria-hidden="true"></i> {{ _("settings.wizard_btn") }}
|
||||
</a>
|
||||
<a href="/database-wizard"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-database mr-1.5" aria-hidden="true"></i> DB Wizard
|
||||
<i class="fas fa-database mr-1.5" aria-hidden="true"></i> {{ _("settings.db_wizard_btn") }}
|
||||
</a>
|
||||
<div class="relative" x-data="{ exportOpen: false }">
|
||||
<button @click="exportOpen = !exportOpen"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-download mr-1.5" aria-hidden="true"></i> Export
|
||||
<i class="fas fa-download mr-1.5" aria-hidden="true"></i> {{ _("settings.export_btn") }}
|
||||
<i class="fas fa-chevron-down ml-1 text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div x-show="exportOpen" @click.outside="exportOpen = false" x-transition
|
||||
@@ -76,18 +76,18 @@
|
||||
<a href="/api/settings/export-env?source=db"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||
@click="exportOpen = false">
|
||||
<i class="fas fa-database mr-2 text-green-600" aria-hidden="true"></i>DB settings only
|
||||
<i class="fas fa-database mr-2 text-green-600" aria-hidden="true"></i>{{ _("settings.export_db_only") }}
|
||||
</a>
|
||||
<a href="/api/settings/export-env?source=effective"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||
@click="exportOpen = false">
|
||||
<i class="fas fa-layer-group mr-2 text-blue-600" aria-hidden="true"></i>Full effective config
|
||||
<i class="fas fa-layer-group mr-2 text-blue-600" aria-hidden="true"></i>{{ _("settings.export_full_config") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/settings/audit-log"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-history mr-1.5" aria-hidden="true"></i> Audit Log
|
||||
<i class="fas fa-history mr-1.5" aria-hidden="true"></i> {{ _("settings.audit_log_btn") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,14 +99,14 @@
|
||||
type="search"
|
||||
x-model="searchQuery"
|
||||
@input="filterSettings()"
|
||||
placeholder="Search settings by name, key, or description…"
|
||||
placeholder="{{ _('settings.search_placeholder') }}"
|
||||
class="w-full pl-10 pr-20 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
aria-label="Search settings"
|
||||
aria-label="{{ _('settings.search_aria_label') }}"
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
<span x-show="searchQuery" x-text="matchCount + ' found'" x-transition class="text-xs text-gray-400"></span>
|
||||
<button x-show="searchQuery" @click="searchQuery = ''; filterSettings()" type="button"
|
||||
class="text-gray-400 hover:text-gray-600" aria-label="Clear search">
|
||||
class="text-gray-400 hover:text-gray-600" aria-label="{{ _('settings.search_clear_aria') }}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -125,9 +125,9 @@
|
||||
<!-- Two-column layout -->
|
||||
<div class="flex gap-6">
|
||||
<!-- Sidebar Navigation (hidden on small screens) -->
|
||||
<aside class="hidden lg:block w-56 flex-shrink-0" aria-label="Settings categories">
|
||||
<aside class="hidden lg:block w-56 flex-shrink-0" aria-label="{{ _('settings.categories_aria') }}">
|
||||
<nav class="settings-sidebar space-y-0.5 bg-white rounded-lg border border-gray-200 p-2 shadow-sm">
|
||||
<p class="px-3 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wider">Categories</p>
|
||||
<p class="px-3 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("settings.categories_heading") }}</p>
|
||||
{% for category, settings_list in settings_data.items() %}
|
||||
<button
|
||||
type="button"
|
||||
@@ -149,11 +149,11 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Mobile category jump (visible on small screens only) -->
|
||||
<div class="lg:hidden mb-4">
|
||||
<label for="mobile-category-select" class="sr-only">Jump to category</label>
|
||||
<label for="mobile-category-select" class="sr-only">{{ _("settings.jump_to_category") }}</label>
|
||||
<select id="mobile-category-select"
|
||||
@change="scrollToCategory($event.target.value)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm shadow-sm">
|
||||
<option value="">Jump to category…</option>
|
||||
<option value="">{{ _("settings.jump_to_category") }}</option>
|
||||
{% for category in settings_data.keys() %}
|
||||
<option value="{{ category }}">{{ category }} ({{ settings_data[category]|length }})</option>
|
||||
{% endfor %}
|
||||
@@ -163,8 +163,8 @@
|
||||
<!-- No results state -->
|
||||
<div x-show="searchQuery && matchCount === 0" class="settings-no-results bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<i class="fas fa-search text-4xl text-gray-300 mb-3"></i>
|
||||
<p class="text-gray-500 text-lg font-medium">No settings found</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Try a different search term or <button type="button" @click="searchQuery = ''; filterSettings()" class="text-blue-500 hover:underline">clear the search</button></p>
|
||||
<p class="text-gray-500 text-lg font-medium">{{ _("settings.no_results_heading") }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">{{ _("settings.no_results_hint") }} <button type="button" @click="searchQuery = ''; filterSettings()" class="text-blue-500 hover:underline">{{ _("settings.no_results_clear") }}</button></p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveSettings">
|
||||
@@ -191,7 +191,7 @@
|
||||
<h2 class="text-lg font-semibold text-gray-800">{{ category }}</h2>
|
||||
<span class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded-full tabular-nums"
|
||||
x-text="(visibleCount['{{ category|e }}'] ?? {{ settings_list|length }}) + ' settings'">
|
||||
{{ settings_list|length }} settings
|
||||
{{ settings_list|length }} {{ _("settings.settings_count_suffix") }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 category-chevron"
|
||||
@@ -225,7 +225,7 @@
|
||||
<span class="text-red-600" title="Restart required">*</span>
|
||||
{% endif %}
|
||||
{% if setting.metadata.required %}
|
||||
<span class="text-red-600 text-xs">(required)</span>
|
||||
<span class="text-red-600 text-xs">{{ _("settings.required_label") }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<!-- Source Badge -->
|
||||
@@ -262,7 +262,7 @@
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="{{ setting.key }}" class="ml-2 text-sm text-gray-700">
|
||||
Enable {{ setting.key.replace('_', ' ').title() }}
|
||||
{{ _("settings.boolean_enable_prefix") }} {{ setting.key.replace('_', ' ').title() }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'slider' %}
|
||||
@@ -302,7 +302,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400">Effective value: <code x-text="formData['{{ setting.key }}'] || '(none)'"></code></p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ _("settings.effective_value_label") }} <code x-text="formData['{{ setting.key }}'] || '(none)'"></code></p>
|
||||
{% elif setting.metadata.type == 'user_autocomplete' %}
|
||||
<!-- User Autocomplete (WCAG 2.1 AA combobox pattern) -->
|
||||
<div class="relative" x-data="userAutocomplete('{{ setting.key }}')" @click.away="showSuggestions = false">
|
||||
@@ -320,7 +320,7 @@
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
@keydown.escape="showSuggestions = false"
|
||||
class="setting-input w-full px-3 py-2 pl-9 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Start typing to search users…"
|
||||
placeholder="{{ _('settings.user_autocomplete_placeholder') }}"
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
@@ -371,11 +371,11 @@
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg px-3 py-2 text-sm text-gray-500"
|
||||
role="status"
|
||||
>
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> No matching users found
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> {{ _("settings.user_autocomplete_empty") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1" id="{{ setting.key }}_help">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Type to search existing users by name, or enter any identifier manually.
|
||||
{{ _("settings.user_autocomplete_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'autocomplete' %}
|
||||
@@ -395,7 +395,7 @@
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
@keydown.escape="showSuggestions = false"
|
||||
class="setting-input w-full px-3 py-2 pl-9 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Type to search or enter a value…"
|
||||
placeholder="{{ _('settings.autocomplete_placeholder') }}"
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
@@ -445,11 +445,11 @@
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg px-3 py-2 text-sm text-gray-500"
|
||||
role="status"
|
||||
>
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> No matches — you can still type a custom value
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> {{ _("settings.autocomplete_no_matches") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Type to search known values, or enter any custom value.
|
||||
{{ _("settings.autocomplete_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'model_picker' %}
|
||||
@@ -462,7 +462,7 @@
|
||||
list="{{ setting.key }}_models"
|
||||
x-model="formData['{{ setting.key }}']"
|
||||
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Select a common model or type a custom name…"
|
||||
placeholder="{{ _('settings.model_picker_placeholder') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<datalist id="{{ setting.key }}_models">
|
||||
@@ -472,7 +472,7 @@
|
||||
</datalist>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Pick from the list or type any model name supported by your provider.
|
||||
{{ _("settings.model_picker_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.options %}
|
||||
@@ -483,7 +483,7 @@
|
||||
x-model="formData['{{ setting.key }}']"
|
||||
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">— select —</option>
|
||||
<option value="">{{ _("common.select_placeholder") }}</option>
|
||||
{% for opt in setting.metadata.options %}
|
||||
<option value="{{ opt }}">{{ opt }}</option>
|
||||
{% endfor %}
|
||||
@@ -503,15 +503,15 @@
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 space-x-2">
|
||||
<span class="text-xs text-gray-400" title="Value is encrypted at rest in database">
|
||||
<span class="text-xs text-gray-400" title="{{ _('settings.encrypted_title') }}">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="togglePassword('{{ setting.key }}')"
|
||||
class="text-gray-400 hover:text-gray-600 focus:outline-none"
|
||||
title="Show/hide value"
|
||||
aria-label="Toggle password visibility"
|
||||
title="{{ _('settings.toggle_visibility_title') }}"
|
||||
aria-label="{{ _('settings.toggle_visibility_aria') }}"
|
||||
>
|
||||
<i :class="showPassword['{{ setting.key }}'] ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
@@ -540,10 +540,10 @@
|
||||
@click="revertSetting('{{ setting.key }}')"
|
||||
:disabled="revertingKey === '{{ setting.key }}'"
|
||||
class="px-3 py-1 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Remove DB override and revert to environment variable or default"
|
||||
title="{{ _('settings.revert_title') }}"
|
||||
>
|
||||
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Remove from DB</span>
|
||||
<span x-show="revertingKey === '{{ setting.key }}'">Reverting…</span>
|
||||
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ _("settings.revert_btn") }}</span>
|
||||
<span x-show="revertingKey === '{{ setting.key }}'">{{ _("settings.reverting") }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -552,10 +552,10 @@
|
||||
@click="saveSetting('{{ setting.key }}')"
|
||||
:disabled="savingKey === '{{ setting.key }}'"
|
||||
class="px-3 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Save this setting"
|
||||
title="{{ _('settings.save_setting_title') }}"
|
||||
>
|
||||
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1" aria-hidden="true"></i>Save</span>
|
||||
<span x-show="savingKey === '{{ setting.key }}'">Saving…</span>
|
||||
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1" aria-hidden="true"></i>{{ _("common.save") }}</span>
|
||||
<span x-show="savingKey === '{{ setting.key }}'">{{ _("settings.saving_state") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,15 +573,15 @@
|
||||
@click="resetForm"
|
||||
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Reset
|
||||
{{ _("common.reset") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!saving">Save All Changes</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
<span x-show="!saving">{{ _("settings.save_all_btn") }}</span>
|
||||
<span x-show="saving">{{ _("settings.saving_state") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
"upload.button_processing": "Processing...",
|
||||
"files.page_title": "File Records",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more – folders are processed recursively",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
@@ -216,7 +216,7 @@
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_label": "Full-Text Search (OCR text, metadata, tags)",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.search_results_empty": "No results found.",
|
||||
@@ -754,5 +754,86 @@
|
||||
"common.prev_page": "Previous page",
|
||||
"common.next_page": "Next page",
|
||||
"common.page": "Page",
|
||||
"common.col_pending": "Pending"
|
||||
"common.col_pending": "Pending",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.files_selected": "files selected",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_files": "files",
|
||||
"files.table_aria": "File records",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.wizard_btn": "Wizard",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.user_autocomplete_empty": "No matching users found",
|
||||
"settings.user_autocomplete_hint": "Type to search existing users by name, or enter any identifier manually.",
|
||||
"settings.user_autocomplete_placeholder": "Start typing to search users…",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.toggle_visibility_title": "Show/hide value",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.ocr_auto": "Auto (use system default)",
|
||||
"pipelines.ocr_language_hint": "Overrides the global language setting for Tesseract/EasyOCR. Azure and Mistral auto-detect the language.",
|
||||
"common.select_placeholder": "— select —"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user