feat(ui): localize status page with i18n translations

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-15 15:56:55 +00:00
parent 19b7eb2ae8
commit e46dadbf45
2 changed files with 200 additions and 118 deletions
+121 -91
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}System Status{% endblock %}
{% block title %}{{ _("status.page_title") }}{% endblock %}
{% block head %}
<!-- Font Awesome is loaded in base.html -->
@@ -8,25 +8,25 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2">System Status</h1>
<h1 class="text-3xl font-bold mb-2">{{ _("status.page_title") }}</h1>
<p class="text-gray-600">
This dashboard shows the status of all configured integrations and targets.
{{ _("status.dashboard_subtitle") }}
</p>
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p><strong>App Version:</strong> {{ app_version }}{% if release_name %} <span class="italic">"{{ release_name }}"</span>{% endif %}</p>
<p><strong>Build Date:</strong> {{ build_date }}</p>
<p><strong>Debug Mode:</strong> {{ "Enabled" if debug_enabled else "Disabled" }}</p>
<p><strong>{{ _("status.app_version") }}:</strong> {{ app_version }}{% if release_name %} <span class="italic">"{{ release_name }}"</span>{% endif %}</p>
<p><strong>{{ _("status.build_date") }}:</strong> {{ build_date }}</p>
<p><strong>{{ _("status.debug_mode") }}:</strong> {{ _("common.enabled") if debug_enabled else _("common.disabled") }}</p>
{% if last_check %}
<p><strong>Last Check:</strong> {{ last_check }}</p>
<p><strong>{{ _("status.last_check") }}:</strong> {{ last_check }}</p>
{% endif %}
{% if container_info.is_docker %}
<p><strong>Container ID:</strong> {{ container_info.id }}</p>
<p><strong>{{ _("status.container_id") }}:</strong> {{ container_info.id }}</p>
{% endif %}
{% if container_info.git_sha and container_info.git_sha != 'Unknown' %}
<p><strong>Git Commit:</strong> {{ container_info.git_sha }}</p>
<p><strong>{{ _("status.git_commit") }}:</strong> {{ container_info.git_sha }}</p>
{% endif %}
{% if container_info.runtime_info %}
<p><strong>Container Started:</strong> {{ container_info.runtime_info }}</p>
<p><strong>{{ _("status.container_started") }}:</strong> {{ container_info.runtime_info }}</p>
{% endif %}
</div>
</div>
@@ -44,11 +44,11 @@
</div>
{% if provider.configured %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Configured
{{ _("status.configured") }}
</span>
{% else %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
Not Configured
{{ _("status.not_configured") }}
</span>
{% endif %}
</div>
@@ -81,14 +81,14 @@
<svg class="-ml-0.5 mr-1.5 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
Active
{{ _("status.active") }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 text-gray-800">
<svg class="-ml-0.5 mr-1.5 h-2 w-2 text-gray-400" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
Inactive
{{ _("status.inactive") }}
</span>
{% endif %}
</div>
@@ -100,7 +100,7 @@
class="view-details-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="{{ name }}"
data-details="{{ provider.details|tojson|forceescape }}">
View Details
{{ _("status.view_details") }}
</button>
{% endif %}
@@ -111,7 +111,7 @@
class="test-generic-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-endpoint="{{ provider.test_endpoint }}"
data-method="POST">
Test Notifications
{{ _("status.test_notifications") }}
</button>
{% endif %}
@@ -121,7 +121,7 @@
class="test-generic-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-endpoint="{{ provider.test_endpoint }}"
data-method="{{ provider.test_method|default('GET') }}">
Test {{ name }}
{{ _("status.test_provider", name=name) }}
</button>
{% endif %}
@@ -131,15 +131,15 @@
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="dropbox">
Test Connection
{{ _("status.test_connection") }}
</button>
<a href="/dropbox-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-gear h-3 w-3 mr-1"></i>
Manage
<i class="fa-solid fa-gear h-3 w-3 mr-1" aria-hidden="true"></i>
{{ _("status.manage") }}
</a>
{% else %}
<a href="/dropbox-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Configure Now
{{ _("status.configure_now") }}
</a>
{% endif %}
{% elif name == "OneDrive" %}
@@ -147,15 +147,15 @@
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="onedrive">
Test Connection
{{ _("status.test_connection") }}
</button>
<a href="/onedrive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-gear h-3 w-3 mr-1"></i>
Manage
<i class="fa-solid fa-gear h-3 w-3 mr-1" aria-hidden="true"></i>
{{ _("status.manage") }}
</a>
{% else %}
<a href="/onedrive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Configure Now
{{ _("status.configure_now") }}
</a>
{% endif %}
{% elif name == "Google Drive" %}
@@ -163,15 +163,15 @@
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="google_drive">
Test Connection
{{ _("status.test_connection") }}
</button>
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-gear h-3 w-3 mr-1"></i>
Manage
<i class="fa-solid fa-gear h-3 w-3 mr-1" aria-hidden="true"></i>
{{ _("status.manage") }}
</a>
{% else %}
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Configure Now
{{ _("status.configure_now") }}
</a>
{% endif %}
{% elif name == "AI Provider" %}
@@ -179,13 +179,13 @@
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="ai_provider">
Test Connection
{{ _("status.test_connection") }}
</button>
<button
id="testAiExtractionBtn"
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-flask mr-1"></i>
Test Extraction
<i class="fa-solid fa-flask mr-1" aria-hidden="true"></i>
{{ _("status.test_extraction") }}
</button>
{% endif %}
{% elif name == "Azure AI" %}
@@ -193,20 +193,20 @@
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="azure">
Test Connection
{{ _("status.test_connection") }}
</button>
{% endif %}
{% elif provider.configured and name == "Paperless-ngx" %}
<a href="{{ provider.details.host }}" target="_blank" class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-external-link h-3 w-3 mr-1"></i>
Open
<i class="fa-solid fa-external-link h-3 w-3 mr-1" aria-hidden="true"></i>
{{ _("status.open") }}
</a>
{% elif provider.configured and name == "NextCloud" %}
{% if provider.details and provider.details.url %}
{% set nextcloud_base_url = provider.details.url.split('/remote.php')[0] %}
<a href="{{ nextcloud_base_url }}" target="_blank" class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-external-link h-3 w-3 mr-1"></i>
Open
<i class="fa-solid fa-external-link h-3 w-3 mr-1" aria-hidden="true"></i>
{{ _("status.open") }}
</a>
{% endif %}
{% endif %}
@@ -219,14 +219,14 @@
</div>
<div class="mt-8 bg-gray-50 p-4 rounded-lg border border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Configuration Settings</h3>
<h3 class="text-lg font-medium text-gray-900">{{ _("status.config_settings") }}</h3>
<p class="text-sm text-gray-600 mt-1">
For more detailed configuration settings and environment variables, check the settings page.
{{ _("status.config_settings_desc") }}
</p>
<div class="mt-4">
<a href="/settings" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
View Detailed Configuration
{{ _("status.view_config") }}
</a>
</div>
</div>
@@ -238,15 +238,15 @@
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
<!-- Icon will be injected by JS -->
</div>
<h3 id="modalTitle" class="text-lg leading-6 font-medium text-gray-900">Success</h3>
<h3 id="modalTitle" class="text-lg leading-6 font-medium text-gray-900">{{ _("status.modal_default_title") }}</h3>
<div class="mt-2 px-7 py-3">
<p id="modalMessage" class="text-sm text-gray-500">
Operation completed successfully.
{{ _("status.modal_default_message") }}
</p>
</div>
<div class="items-center px-4 py-3">
<button id="modalClose" class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-300">
Close
{{ _("common.close") }}
</button>
</div>
</div>
@@ -258,12 +258,12 @@
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="absolute top-0 right-0 pt-4 pr-4">
<button type="button" id="closeDetailsModal" class="text-gray-400 hover:text-gray-500">
<span class="sr-only">Close</span>
<i class="fa-solid fa-xmark h-6 w-6"></i>
<span class="sr-only">{{ _("common.close") }}</span>
<i class="fa-solid fa-xmark h-6 w-6" aria-hidden="true"></i>
</button>
</div>
<div class="mt-3">
<h3 id="detailsModalTitle" class="text-lg leading-6 font-medium text-gray-900 mb-4">Provider Details</h3>
<h3 id="detailsModalTitle" class="text-lg leading-6 font-medium text-gray-900 mb-4">{{ _("status.provider_details") }}</h3>
<div class="mt-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div id="detailsContent" class="col-span-2">
@@ -273,7 +273,7 @@
</div>
<div class="mt-6 flex justify-end">
<button type="button" id="closeDetailsBtn" class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600">
Close
{{ _("common.close") }}
</button>
</div>
</div>
@@ -285,35 +285,34 @@
<div class="relative top-10 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-2/3 xl:w-1/2 shadow-lg rounded-md bg-white">
<div class="absolute top-0 right-0 pt-4 pr-4">
<button type="button" id="closeAiExtractionModal" class="text-gray-400 hover:text-gray-500">
<span class="sr-only">Close</span>
<i class="fa-solid fa-xmark h-6 w-6"></i>
<span class="sr-only">{{ _("common.close") }}</span>
<i class="fa-solid fa-xmark h-6 w-6" aria-hidden="true"></i>
</button>
</div>
<div class="mt-3">
<h3 id="aiExtractionModalTitle" class="text-lg leading-6 font-medium text-gray-900 mb-1">
<i class="fa-solid fa-flask mr-2 text-indigo-600"></i>AI Extraction Test
<i class="fa-solid fa-flask mr-2 text-indigo-600" aria-hidden="true"></i>{{ _("status.ai_extraction_title") }}
</h3>
<p class="text-sm text-gray-500 mb-4">
Paste the plain-text content of a document below and run it through the configured AI provider
to inspect the raw response, extracted JSON, and tags.
{{ _("status.ai_extraction_desc") }}
</p>
<!-- Input area -->
<div id="aiExtractionInput">
<label for="aiExtractionText" class="block text-sm font-medium text-gray-700 mb-1">Document Text</label>
<label for="aiExtractionText" class="block text-sm font-medium text-gray-700 mb-1">{{ _("status.ai_extraction_label") }}</label>
<textarea
id="aiExtractionText"
rows="10"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-mono focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Paste the plain-text content of your document here…"></textarea>
placeholder="{{ _('status.ai_extraction_placeholder') }}"></textarea>
<div class="mt-3 flex justify-end space-x-2">
<button type="button" id="cancelAiExtractionBtn"
class="px-4 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-indigo-500">
Cancel
{{ _("common.cancel") }}
</button>
<button type="button" id="runAiExtractionBtn"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fa-solid fa-play mr-2"></i>Run Extraction
<i class="fa-solid fa-play mr-2" aria-hidden="true"></i>{{ _("status.run_extraction") }}
</button>
</div>
</div>
@@ -328,30 +327,30 @@
<!-- Raw response -->
<div>
<h4 class="text-sm font-medium text-gray-900 mb-1">Raw LLM Response</h4>
<h4 class="text-sm font-medium text-gray-900 mb-1">{{ _("status.raw_llm_response") }}</h4>
<pre id="aiRawResponse" class="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-auto max-h-48 whitespace-pre-wrap break-words"></pre>
</div>
<!-- Parsed JSON -->
<div id="aiParsedJsonSection">
<h4 class="text-sm font-medium text-gray-900 mb-1">Parsed JSON</h4>
<h4 class="text-sm font-medium text-gray-900 mb-1">{{ _("status.parsed_json_label") }}</h4>
<pre id="aiParsedJson" class="bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-auto max-h-64 whitespace-pre-wrap break-words"></pre>
</div>
<!-- Tags -->
<div id="aiTagsSection">
<h4 class="text-sm font-medium text-gray-900 mb-1">Extracted Tags</h4>
<h4 class="text-sm font-medium text-gray-900 mb-1">{{ _("status.extracted_tags") }}</h4>
<div id="aiTags" class="flex flex-wrap gap-2"></div>
</div>
<div class="flex justify-end space-x-2 pt-2">
<button type="button" id="aiExtractionBackBtn"
class="px-4 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-indigo-500">
<i class="fa-solid fa-arrow-left mr-1"></i>Back
<i class="fa-solid fa-arrow-left mr-1" aria-hidden="true"></i>{{ _("common.back") }}
</button>
<button type="button" id="aiExtractionCloseBtn"
class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600">
Close
{{ _("common.close") }}
</button>
</div>
</div>
@@ -363,6 +362,37 @@
{% block scripts %}
<script>
/* Translated strings for JavaScript use */
const i18nStrings = {
providerConfigDetails: {{ _("status.provider_config_details") | tojson }},
noDetails: {{ _("status.no_details") | tojson }},
settingLabel: {{ _("status.setting_label") | tojson }},
valueLabel: {{ _("status.value_label") | tojson }},
testNotificationSent: {{ _("status.test_notification_sent") | tojson }},
notificationConfigMissing: {{ _("status.notification_config_missing") | tojson }},
testNotificationFailed: {{ _("status.test_notification_failed") | tojson }},
errorTestingNotifications: {{ _("status.error_testing_notifications") | tojson }},
testSuccessful: {{ _("status.test_successful") | tojson }},
tokenValidFor: {{ _("status.token_valid_for") | tojson }},
warning: {{ _("common.warning") | tojson }},
authRequired: {{ _("status.auth_required") | tojson }},
tokenExpired: {{ _("status.token_expired") | tojson }},
testFailed: {{ _("status.test_failed") | tojson }},
connectionError: {{ _("status.connection_error") | tojson }},
connectionTestSuccessful: {{ _("status.connection_test_successful") | tojson }},
connectionTestFailed: {{ _("status.connection_test_failed") | tojson }},
errorTestingConnection: {{ _("status.error_testing_connection") | tojson }},
sending: {{ _("status.sending") | tojson }},
testing: {{ _("status.testing") | tojson }},
jsonParseIssue: {{ _("status.json_parse_issue") | tojson }},
aiExtractionFailed: {{ _("status.ai_extraction_failed") | tojson }},
errorRunningExtraction: {{ _("status.error_running_extraction") | tojson }},
running: {{ _("status.running") | tojson }},
aiEmptyResponse: {{ _("status.ai_empty_response") | tojson }},
asAccount: {{ _("status.as_account") | tojson }},
configureNow: {{ _("status.configure_now") | tojson }},
};
document.addEventListener('DOMContentLoaded', function() {
// Modal elements
const resultModal = document.getElementById('resultModal');
@@ -400,7 +430,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
function showDetailsModal(providerName, details) {
detailsModalTitle.textContent = providerName + ' Configuration Details';
detailsModalTitle.textContent = i18nStrings.providerConfigDetails.replace('{name}', providerName);
// Clear previous content
detailsContent.innerHTML = '';
@@ -414,8 +444,8 @@ document.addEventListener('DOMContentLoaded', function() {
thead.className = 'bg-gray-50';
thead.innerHTML = `
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Setting</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">${i18nStrings.settingLabel}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">${i18nStrings.valueLabel}</th>
</tr>
`;
@@ -456,7 +486,7 @@ document.addEventListener('DOMContentLoaded', function() {
table.appendChild(tbody);
detailsContent.appendChild(table);
} else {
detailsContent.innerHTML = '<p class="text-sm text-gray-500">No details available</p>';
detailsContent.innerHTML = `<p class="text-sm text-gray-500">${i18nStrings.noDetails}</p>`;
}
detailsModal.classList.remove('hidden');
@@ -510,7 +540,7 @@ document.addEventListener('DOMContentLoaded', function() {
testNotificationBtn.addEventListener('click', function() {
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Sending...';
this.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> ${i18nStrings.sending}`;
this.disabled = true;
fetch('/api/diagnostic/test-notification', {
@@ -519,15 +549,15 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showModal('success', 'Test Notification Sent', data.message);
showModal('success', i18nStrings.testNotificationSent, data.message);
} else if (data.status === 'warning') {
showModal('error', 'Notification Configuration Missing', data.message);
showModal('error', i18nStrings.notificationConfigMissing, data.message);
} else {
showModal('error', 'Test Notification Failed', data.message);
showModal('error', i18nStrings.testNotificationFailed, data.message);
}
})
.catch(error => {
showModal('error', 'Connection Error', 'Error testing notifications: ' + error.message);
showModal('error', i18nStrings.connectionError, i18nStrings.errorTestingNotifications + error.message);
})
.finally(() => {
// Always restore the button text and enable the button, regardless of success or failure
@@ -545,7 +575,7 @@ document.addEventListener('DOMContentLoaded', function() {
const endpoint = this.getAttribute('data-endpoint');
const method = this.getAttribute('data-method') || 'GET';
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Testing...';
this.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> ${i18nStrings.testing}`;
this.disabled = true;
fetch(endpoint, {
@@ -558,28 +588,28 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.token_info && data.token_info.expires_in_human) {
let message = data.message || 'Connection successful';
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${data.token_info.expires_in_human}
</div>`;
modalTitle.textContent = 'Test Successful';
modalTitle.textContent = i18nStrings.testSuccessful;
modalMessage.innerHTML = message;
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
resultModal.classList.remove('hidden');
} else {
// Regular success
showModal('success', 'Test Successful', data.message + (data.account ? ' as ' + data.account : ''));
showModal('success', i18nStrings.testSuccessful, data.message + (data.account ? ' ' + i18nStrings.asAccount + ' ' + data.account : ''));
}
} else if (data.status === 'warning') {
showModal('error', 'Warning', data.message);
showModal('error', i18nStrings.warning, data.message);
} else if (data.needs_reauth) {
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure this connection.');
showModal('error', i18nStrings.authRequired, i18nStrings.tokenExpired);
} else {
showModal('error', 'Test Failed', data.message);
showModal('error', i18nStrings.testFailed, data.message);
}
})
.catch(error => {
showModal('error', 'Connection Error', `Error: ${error.message}`);
showModal('error', i18nStrings.connectionError, `Error: ${error.message}`);
})
.finally(() => {
// Always restore the button state
@@ -596,7 +626,7 @@ document.addEventListener('DOMContentLoaded', function() {
const provider = this.getAttribute('data-provider');
const originalText = this.textContent;
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Testing...';
this.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> ${i18nStrings.testing}`;
this.disabled = true;
let endpoint = '';
@@ -622,35 +652,35 @@ document.addEventListener('DOMContentLoaded', function() {
// Add token expiration info if available (especially for Google Drive)
if (data.token_info && data.token_info.expires_in_human) {
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${data.token_info.expires_in_human}
</div>`;
// Show the message with HTML
modalTitle.textContent = 'Connection Test Successful';
modalTitle.textContent = i18nStrings.connectionTestSuccessful;
modalMessage.innerHTML = message;
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
resultModal.classList.remove('hidden');
} else {
// Use the regular modal without HTML
showModal('success', 'Connection Test Successful', `${data.message} ${data.account ? 'as ' + data.account : ''}`);
showModal('success', i18nStrings.connectionTestSuccessful, `${data.message} ${data.account ? i18nStrings.asAccount + ' ' + data.account : ''}`);
}
} else {
if (data.needs_reauth) {
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure this connection.');
showModal('error', i18nStrings.authRequired, i18nStrings.tokenExpired);
// Add option to go to config page
modalClose.textContent = "Configure Now";
modalClose.textContent = i18nStrings.configureNow;
modalClose.addEventListener('click', function redirectToConfig() {
window.location.href = `/${provider.replace('_', '-')}-setup`;
modalClose.removeEventListener('click', redirectToConfig);
}, { once: true });
} else {
showModal('error', 'Connection Test Failed', data.message);
showModal('error', i18nStrings.connectionTestFailed, data.message);
}
}
})
.catch(error => {
showModal('error', 'Connection Error', 'Error testing connection: ' + error.message);
showModal('error', i18nStrings.connectionError, i18nStrings.errorTestingConnection + error.message);
})
.finally(() => {
this.textContent = originalText;
@@ -696,11 +726,11 @@ document.addEventListener('DOMContentLoaded', function() {
aiExtractionMeta.textContent = `Provider: ${data.provider || '—'} | Model: ${data.model || '—'}`;
// Raw response
aiRawResponse.textContent = data.raw_response || '(empty)';
aiRawResponse.textContent = data.raw_response || i18nStrings.aiEmptyResponse;
// Parse warning
if (data.parse_error) {
aiParseWarning.textContent = `JSON parse issue: ${data.parse_error}`;
aiParseWarning.textContent = i18nStrings.jsonParseIssue + data.parse_error;
aiParseWarning.classList.remove('hidden');
} else {
aiParseWarning.classList.add('hidden');
@@ -775,7 +805,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
const originalHTML = runAiExtractionBtn.innerHTML;
runAiExtractionBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i>Running…';
runAiExtractionBtn.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i>${i18nStrings.running}`;
runAiExtractionBtn.disabled = true;
cancelAiExtractionBtn.disabled = true;
@@ -790,12 +820,12 @@ document.addEventListener('DOMContentLoaded', function() {
showAiExtractionResults(data);
} else {
closeAiExtractionModalFn();
showModal('error', 'AI Extraction Failed', data.message || 'Unknown error');
showModal('error', i18nStrings.aiExtractionFailed, data.message || 'Unknown error');
}
})
.catch(error => {
closeAiExtractionModalFn();
showModal('error', 'Connection Error', 'Error running extraction: ' + error.message);
showModal('error', i18nStrings.connectionError, i18nStrings.errorRunningExtraction + error.message);
})
.finally(() => {
runAiExtractionBtn.innerHTML = originalHTML;
+79 -27
View File
@@ -41,19 +41,6 @@
"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_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.",
"attribution.heading": "Third-Party Software Attributions",
"attribution.intro": "DocuElevate uses several open source libraries and tools. We are grateful to the developers of these projects for their contributions to open source software.",
"attribution.page_title": "DocuElevate - Third-Party Attributions",
"attribution.paramiko_lgpl_note": "Note: This library is licensed under the GNU Lesser General Public License v2.1 (LGPL-2.1)",
"attribution.section_docker": "Docker Images",
"attribution.section_frontend": "Frontend Dependencies",
"attribution.section_python": "Python Dependencies",
"attribution.special_lgpl_link": "here",
"attribution.special_lgpl_post": ".",
"attribution.special_lgpl_pre": "A copy of the LGPL license can be found",
"attribution.special_source_post": ".",
"attribution.special_source_pre": "This software includes Paramiko, which is licensed under LGPL. The source code for Paramiko is available at",
"attribution.special_title": "Special Attribution:",
"api_tokens.col_created": "Created",
"api_tokens.col_last_ip": "Last IP",
"api_tokens.col_last_used": "Last Used",
@@ -86,6 +73,19 @@
"api_tokens.usage_intro_pre": "Use your API token in the",
"api_tokens.your_tokens": "Your Tokens",
"app.name": "DocuElevate",
"attribution.heading": "Third-Party Software Attributions",
"attribution.intro": "DocuElevate uses several open source libraries and tools. We are grateful to the developers of these projects for their contributions to open source software.",
"attribution.page_title": "DocuElevate - Third-Party Attributions",
"attribution.paramiko_lgpl_note": "Note: This library is licensed under the GNU Lesser General Public License v2.1 (LGPL-2.1)",
"attribution.section_docker": "Docker Images",
"attribution.section_frontend": "Frontend Dependencies",
"attribution.section_python": "Python Dependencies",
"attribution.special_lgpl_link": "here",
"attribution.special_lgpl_post": ".",
"attribution.special_lgpl_pre": "A copy of the LGPL license can be found",
"attribution.special_source_post": ".",
"attribution.special_source_pre": "This software includes Paramiko, which is licensed under LGPL. The source code for Paramiko is available at",
"attribution.special_title": "Special Attribution:",
"audit.col_ip": "IP",
"audit.col_resource": "Resource",
"audit.col_timestamp": "Timestamp",
@@ -295,7 +295,7 @@
"cookie_policy.s4_privacy_link": "Privacy Notice",
"cookie_policy.s5_heading": "Managing Cookies",
"cookie_policy.s5_p1": "Most web browsers allow you to control cookies through their settings. However, blocking or deleting our session cookies will prevent DocuElevate from functioning, as user authentication relies on these cookies.",
"cookie_policy.s5_p2": "You may also clear the cookie notice acknowledgement stored in your browser's localStorage at any time via your browser's developer tools (Application \u2192 Local Storage).",
"cookie_policy.s5_p2": "You may also clear the cookie notice acknowledgement stored in your browser's localStorage at any time via your browser's developer tools (Application Local Storage).",
"cookie_policy.s5_p3_and": "and",
"cookie_policy.s5_p3_pre": "This Cookie Policy is part of and incorporated into our",
"cookie_policy.s5_privacy_link": "Privacy Notice",
@@ -764,19 +764,6 @@
"integrations.webhook_security_tip": "Create a dedicated API token for each integration and revoke it immediately if compromised. Tokens can be managed on the API Tokens page.",
"integrations.webhook_step1": "Go to {api_tokens_link} and create a personal token.",
"integrations.webhook_upload_with_token": "Use the token to upload documents via the API:",
"license.apache_description": "DocuElevate is distributed under the Apache License 2.0, which is a permissive open-source software license that allows you to use, modify, distribute, and contribute to the project.",
"license.apache_heading": "Apache License 2.0",
"license.heading": "License Information",
"license.page_title": "License Information - DocuElevate",
"license.related_about_link": "About page",
"license.related_and": "and",
"license.related_heading": "Related Information",
"license.related_p1_post": "for information about using the DocuElevate service.",
"license.related_p1_pre": "While this license governs the use of our software, please also review our",
"license.related_p2_post": ".",
"license.related_p2_pre": "For more information about DocuElevate, please visit the",
"license.related_privacy_link": "Privacy Policy",
"license.related_terms_link": "Terms of Service",
"language.bg": "Български",
"language.ca": "Català",
"language.change_success": "Language changed to {language}",
@@ -814,6 +801,19 @@
"language.tr": "Türkçe",
"language.uk": "Українська",
"language.zh": "中文",
"license.apache_description": "DocuElevate is distributed under the Apache License 2.0, which is a permissive open-source software license that allows you to use, modify, distribute, and contribute to the project.",
"license.apache_heading": "Apache License 2.0",
"license.heading": "License Information",
"license.page_title": "License Information - DocuElevate",
"license.related_about_link": "About page",
"license.related_and": "and",
"license.related_heading": "Related Information",
"license.related_p1_post": "for information about using the DocuElevate service.",
"license.related_p1_pre": "While this license governs the use of our software, please also review our",
"license.related_p2_post": ".",
"license.related_p2_pre": "For more information about DocuElevate, please visit the",
"license.related_privacy_link": "Privacy Policy",
"license.related_terms_link": "Terms of Service",
"nav.about": "About",
"nav.account_menu": "Account menu",
"nav.account_menu_for": "Account menu for {name}",
@@ -1305,14 +1305,66 @@
"similarity.subtitle": "Pairs of documents with high semantic similarity, ranked by score.",
"similarity.trigger_aria": "Trigger embedding computation for all files missing embeddings",
"similarity.trigger_now": "trigger it now",
"status.active": "Active",
"status.ai_empty_response": "(empty)",
"status.ai_extraction_desc": "Paste the plain-text content of a document below and run it through the configured AI provider to inspect the raw response, extracted JSON, and tags.",
"status.ai_extraction_failed": "AI Extraction Failed",
"status.ai_extraction_label": "Document Text",
"status.ai_extraction_placeholder": "Paste the plain-text content of your document here…",
"status.ai_extraction_title": "AI Extraction Test",
"status.app_version": "App Version",
"status.as_account": "as",
"status.auth_required": "Authentication Required",
"status.build_date": "Build Date",
"status.config_settings": "Configuration Settings",
"status.config_settings_desc": "For more detailed configuration settings and environment variables, check the settings page.",
"status.configure_now": "Configure Now",
"status.configured": "Configured",
"status.connection_error": "Connection Error",
"status.connection_test_failed": "Connection Test Failed",
"status.connection_test_successful": "Connection Test Successful",
"status.container_id": "Container ID",
"status.container_started": "Container Started",
"status.dashboard_subtitle": "This dashboard shows the status of all configured integrations and targets.",
"status.debug_mode": "Debug Mode",
"status.error_running_extraction": "Error running extraction: ",
"status.error_testing_connection": "Error testing connection: ",
"status.error_testing_notifications": "Error testing notifications: ",
"status.extracted_tags": "Extracted Tags",
"status.git_commit": "Git Commit",
"status.inactive": "Inactive",
"status.json_parse_issue": "JSON parse issue: ",
"status.last_check": "Last Check",
"status.manage": "Manage",
"status.modal_default_message": "Operation completed successfully.",
"status.modal_default_title": "Success",
"status.no_details": "No details available",
"status.not_configured": "Not Configured",
"status.notification_config_missing": "Notification Configuration Missing",
"status.open": "Open",
"status.page_title": "System Status",
"status.parsed_json_label": "Parsed JSON",
"status.provider_config_details": "{name} Configuration Details",
"status.provider_details": "Provider Details",
"status.raw_llm_response": "Raw LLM Response",
"status.run_extraction": "Run Extraction",
"status.running": "Running…",
"status.sending": "Sending...",
"status.setting_label": "Setting",
"status.test_connection": "Test Connection",
"status.test_extraction": "Test Extraction",
"status.test_failed": "Test Failed",
"status.test_notification_failed": "Test Notification Failed",
"status.test_notification_sent": "Test Notification Sent",
"status.test_notifications": "Test Notifications",
"status.test_provider": "Test {name}",
"status.test_successful": "Test Successful",
"status.testing": "Testing...",
"status.token_expired": "Your token has expired or is invalid. Please reconfigure this connection.",
"status.token_valid_for": "Token valid for:",
"status.value_label": "Value",
"status.view_config": "View Detailed Configuration",
"status.view_details": "View Details",
"subscription.available_plans_heading": "Available Plans",
"subscription.back_to_dashboard": "Back to Dashboard",
"subscription.cancel_pending": "Cancel change",