Merge branch 'main' into copilot/add-conditional-routing
Resolve conflicts in app/api/__init__.py and app/models.py. Renumber migration 027_add_routing_rules → 035_add_routing_rules. Fix migration chain: down_revision → 034_add_user_profile_settings. Add PipelineRoutingRule to migrations/env.py.
This commit is contained in:
@@ -5,6 +5,7 @@ REDIS_URL=redis://redis:6379/0
|
||||
EXTERNAL_HOSTNAME=docuelevate.example.com
|
||||
GOTENBERG_URL=http://gotenberg:3000
|
||||
ALLOW_FILE_DELETE=true # Allow deletion of file records
|
||||
COMPLIANCE_ENABLED=true # Enable compliance templates dashboard (GDPR, HIPAA, SOC 2)
|
||||
|
||||
# **UI / Appearance**
|
||||
# Default colour scheme: system (follow OS), light, or dark
|
||||
@@ -96,6 +97,22 @@ MAX_UPLOAD_SIZE=1073741824
|
||||
# Allowed request headers (use * to allow all)
|
||||
# CORS_ALLOWED_HEADERS=*
|
||||
|
||||
# **Audit Logging & SIEM Integration** (see docs/ConfigurationGuide.md#audit-logging)
|
||||
# Enable HTTP request audit logging middleware
|
||||
AUDIT_LOGGING_ENABLED=true
|
||||
# Include client IP in audit log entries (disable for GDPR-sensitive deployments)
|
||||
AUDIT_LOG_INCLUDE_CLIENT_IP=true
|
||||
|
||||
# Forward audit events to an external SIEM system (Syslog, Splunk, Logstash, Grafana, etc.)
|
||||
# AUDIT_SIEM_ENABLED=false
|
||||
# AUDIT_SIEM_TRANSPORT=syslog # syslog | http
|
||||
# AUDIT_SIEM_SYSLOG_HOST=localhost
|
||||
# AUDIT_SIEM_SYSLOG_PORT=514
|
||||
# AUDIT_SIEM_SYSLOG_PROTOCOL=udp # udp | tcp
|
||||
# AUDIT_SIEM_HTTP_URL= # e.g. https://splunk:8088/services/collector/event
|
||||
# AUDIT_SIEM_HTTP_TOKEN= # Bearer / HEC token
|
||||
# AUDIT_SIEM_HTTP_CUSTOM_HEADERS= # Comma-separated Key:Value pairs
|
||||
|
||||
# **Rate Limiting** (see SECURITY_AUDIT.md and docs/API.md)
|
||||
# Protects against DoS attacks and API abuse by limiting request rates per IP/user
|
||||
# Enabled by default - highly recommended for production
|
||||
@@ -158,6 +175,33 @@ AUTHENTIK_CLIENT_SECRET=<yourAuthentikClientSecret>
|
||||
AUTHENTIK_CONFIG_URL=<ConfigUrlOfYourApp, e.g. https://authentik.example.com/application/o/docuelevate/.well-known/openid-configuration>
|
||||
OAUTH_PROVIDER_NAME="Authentik SSO"
|
||||
|
||||
# **Social Login Providers**
|
||||
# Enable one or more social login providers to let users sign in with existing accounts.
|
||||
# Each provider requires separate OAuth credentials. See docs/SocialLoginSetup.md for details.
|
||||
|
||||
# Google Sign-In (https://console.cloud.google.com/apis/credentials)
|
||||
# SOCIAL_AUTH_GOOGLE_ENABLED=false
|
||||
# SOCIAL_AUTH_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
|
||||
# SOCIAL_AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
# Microsoft Sign-In / Azure AD (https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps)
|
||||
# SOCIAL_AUTH_MICROSOFT_ENABLED=false
|
||||
# SOCIAL_AUTH_MICROSOFT_CLIENT_ID=your-microsoft-application-id
|
||||
# SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
|
||||
# SOCIAL_AUTH_MICROSOFT_TENANT=common # common | organizations | consumers | <tenant-id>
|
||||
|
||||
# Apple Sign-In (https://developer.apple.com/account/resources)
|
||||
# SOCIAL_AUTH_APPLE_ENABLED=false
|
||||
# SOCIAL_AUTH_APPLE_CLIENT_ID=com.example.docuelevate
|
||||
# SOCIAL_AUTH_APPLE_TEAM_ID=ABCDE12345
|
||||
# SOCIAL_AUTH_APPLE_KEY_ID=FGHIJ67890
|
||||
# SOCIAL_AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
|
||||
# Dropbox Sign-In (https://www.dropbox.com/developers/apps)
|
||||
# SOCIAL_AUTH_DROPBOX_ENABLED=false
|
||||
# SOCIAL_AUTH_DROPBOX_CLIENT_ID=your-dropbox-app-key
|
||||
# SOCIAL_AUTH_DROPBOX_CLIENT_SECRET=your-dropbox-app-secret
|
||||
|
||||
# **AI/ML Services**
|
||||
# Select your AI provider: openai | azure | anthropic | gemini | ollama | openrouter | portkey | litellm
|
||||
AI_PROVIDER=openai
|
||||
@@ -210,6 +254,7 @@ EMAIL_SENDER=DocuElevate System <docuelevate@example.com>
|
||||
# These settings are intentionally separate from the shared EMAIL_* settings above.
|
||||
# Configuring EMAIL_HOST for password reset / notifications does NOT automatically
|
||||
# enable the email destination – you must set DEST_EMAIL_HOST to activate it.
|
||||
# DEST_EMAIL_ENABLED=true # Set to false to disable email delivery without removing credentials
|
||||
DEST_EMAIL_HOST=smtp.example.com
|
||||
DEST_EMAIL_PORT=587
|
||||
DEST_EMAIL_USERNAME=docuelevate@example.com
|
||||
@@ -294,8 +339,15 @@ IMAP2_DELETE_AFTER_PROCESS=false
|
||||
# Use for pre-production instances that share a mailbox with production.
|
||||
IMAP_READONLY_MODE=false
|
||||
|
||||
# Controls which attachment types are ingested from IMAP emails.
|
||||
# 'documents_only' (default) – PDFs and office files only; images are skipped.
|
||||
# 'all' – all supported file types including images.
|
||||
# Per-user IMAP accounts can override this global default.
|
||||
IMAP_ATTACHMENT_FILTER=documents_only
|
||||
|
||||
# **Storage/Document Services**
|
||||
# Amazon S3
|
||||
# S3_ENABLED=true # Set to false to disable S3 uploads without removing credentials
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
@@ -305,12 +357,14 @@ S3_STORAGE_CLASS=STANDARD
|
||||
S3_ACL=private
|
||||
|
||||
# NextCloud
|
||||
# NEXTCLOUD_ENABLED=true # Set to false to disable NextCloud uploads without removing credentials
|
||||
NEXTCLOUD_UPLOAD_URL=https://nextcloud.example.com/remote.php/dav/files/<USERNAME>
|
||||
NEXTCLOUD_FOLDER="<NEXTCLOUD_FOLDER_PATH>"
|
||||
NEXTCLOUD_USERNAME=<NEXTCLOUD_USERNAME>
|
||||
NEXTCLOUD_PASSWORD=<NEXTCLOUD_PASSWORD>
|
||||
|
||||
# Paperless-ngx
|
||||
# PAPERLESS_ENABLED=true # Set to false to disable Paperless uploads without removing credentials
|
||||
PAPERLESS_HOST=https://paperless.example.com
|
||||
PAPERLESS_NGX_API_TOKEN=<PAPERLESS_API_TOKEN>
|
||||
# Optional: Name of the custom field in Paperless-ngx to store the "absender" (sender) value
|
||||
@@ -327,12 +381,14 @@ PAPERLESS_NGX_API_TOKEN=<PAPERLESS_API_TOKEN>
|
||||
# PAPERLESS_CUSTOM_FIELDS_MAPPING=
|
||||
|
||||
# Dropbox
|
||||
# DROPBOX_ENABLED=true # Set to false to disable Dropbox uploads without removing credentials
|
||||
DROPBOX_APP_KEY=<DROPBOX_APP_KEY>
|
||||
DROPBOX_APP_SECRET=<DROPBOX_APP_SECRET>
|
||||
DROPBOX_REFRESH_TOKEN=<DROPBOX_REFRESH_TOKEN>
|
||||
DROPBOX_FOLDER="/Documents/Uploads"
|
||||
|
||||
# Google Drive
|
||||
# GOOGLE_DRIVE_ENABLED=true # Set to false to disable Google Drive uploads without removing credentials
|
||||
# Service Account Method:
|
||||
GOOGLE_DRIVE_CREDENTIALS_JSON={"type":"service_account","project_id":"your-project","private_key_id":"key-id","private_key":"-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\n","client_email":"service-account@project.iam.gserviceaccount.com","client_id":"client-id","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/service-account%40project.iam.gserviceaccount.com"}
|
||||
GOOGLE_DRIVE_FOLDER_ID=<YOUR_FOLDER_ID>
|
||||
@@ -345,6 +401,7 @@ GOOGLE_DRIVE_CLIENT_SECRET=your-oauth-client-secret # Required for OAuth method
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN=your-oauth-refresh-token # Required for OAuth method
|
||||
|
||||
# OneDrive
|
||||
# ONEDRIVE_ENABLED=true # Set to false to disable OneDrive uploads without removing credentials
|
||||
ONEDRIVE_CLIENT_ID=your-client-id
|
||||
ONEDRIVE_CLIENT_SECRET=your-client-secret
|
||||
ONEDRIVE_TENANT_ID=common
|
||||
@@ -352,6 +409,7 @@ ONEDRIVE_REFRESH_TOKEN=your-refresh-token
|
||||
ONEDRIVE_FOLDER_PATH=Documents/Uploads
|
||||
|
||||
# WebDAV
|
||||
# WEBDAV_ENABLED=true # Set to false to disable WebDAV uploads without removing credentials
|
||||
WEBDAV_URL=https://webdav.example.com/path
|
||||
WEBDAV_USERNAME=webdav_user
|
||||
WEBDAV_PASSWORD=your_secure_webdav_password
|
||||
@@ -359,6 +417,7 @@ WEBDAV_FOLDER=/Documents/Uploads
|
||||
WEBDAV_VERIFY_SSL=True
|
||||
|
||||
# FTP
|
||||
# FTP_ENABLED=true # Set to false to disable FTP uploads without removing credentials
|
||||
# Security Note: FTP_USE_TLS=True is strongly recommended for secure connections
|
||||
# Set FTP_ALLOW_PLAINTEXT=False in production to prevent unencrypted FTP
|
||||
FTP_HOST=ftp.example.com
|
||||
@@ -370,6 +429,7 @@ FTP_USE_TLS=True
|
||||
FTP_ALLOW_PLAINTEXT=True
|
||||
|
||||
# SFTP
|
||||
# SFTP_ENABLED=true # Set to false to disable SFTP uploads without removing credentials
|
||||
# Security Note: Host key verification is enabled by default (False)
|
||||
# Only set to True in development/testing environments if needed
|
||||
# When false, configure SSH known_hosts for proper host key verification
|
||||
@@ -382,6 +442,16 @@ SFTP_PASSWORD=your_secure_sftp_password
|
||||
SFTP_FOLDER=/Documents/Uploads
|
||||
SFTP_DISABLE_HOST_KEY_VERIFICATION=False # Default is False (secure); set to True only for testing
|
||||
|
||||
# iCloud Drive
|
||||
# ICLOUD_ENABLED=true # Set to false to disable iCloud uploads without removing credentials
|
||||
# Requires an Apple ID with iCloud Drive enabled.
|
||||
# For accounts with two-factor authentication (most accounts), generate an
|
||||
# app-specific password at https://appleid.apple.com/account/manage
|
||||
ICLOUD_USERNAME=your_apple_id@example.com
|
||||
ICLOUD_PASSWORD=your-app-specific-password
|
||||
ICLOUD_FOLDER=Documents/Uploads
|
||||
# ICLOUD_COOKIE_DIRECTORY=/path/to/cookie/dir # Optional: defaults to ~/.pyicloud
|
||||
|
||||
# **HTTP Request Settings**
|
||||
# Timeout for HTTP requests - set higher to handle large PDF files (up to 1GB)
|
||||
HTTP_REQUEST_TIMEOUT=120 # Timeout in seconds (default: 120 for large file operations)
|
||||
@@ -510,3 +580,12 @@ EMBEDDING_MAX_TOKENS=8000
|
||||
# Attach PII (IP addresses, user agents) to Sentry events.
|
||||
# Disable (default) to stay GDPR/CCPA compliant.
|
||||
# SENTRY_SEND_DEFAULT_PII=false
|
||||
|
||||
# **Mobile App – Push Notifications**
|
||||
# Push notifications are delivered via Expo's push notification service
|
||||
# (https://expo.dev/notifications) which routes to APNs (iOS) and FCM (Android).
|
||||
# No additional credentials are required on the server side.
|
||||
# The mobile app registers its Expo push token via POST /api/mobile/register-device.
|
||||
#
|
||||
# To use native FCM/APNs directly (without Expo relay), replace the
|
||||
# send_expo_push_notification function in app/utils/push_notification.py.
|
||||
|
||||
@@ -17,6 +17,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: christianlouis/docuelevate
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
schedule:
|
||||
- cron: '37 1 * * 1'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
|
||||
@@ -12,6 +12,9 @@ permissions:
|
||||
pull-requests: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Semantic Release
|
||||
|
||||
@@ -16,6 +16,9 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
ruff-auto-fix:
|
||||
name: Auto-fix Ruff Issues
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-03-09T23:00:57Z
|
||||
2026-03-12T21:44:42Z
|
||||
|
||||
+269
@@ -10,6 +10,275 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
<!-- version list -->
|
||||
|
||||
## v0.125.0 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **profile**: Address code review feedback - early size check, CSRF helper, test constants
|
||||
([`9583d6d`](https://github.com/christianlouis/DocuElevate/commit/9583d6d96f02d3ee105c73a47084661feef3e811))
|
||||
|
||||
- **tests**: Pass mock db to whoami_handler in test_api_auth_enabled.py
|
||||
([`270e1a5`](https://github.com/christianlouis/DocuElevate/commit/270e1a56b51148651ed499780ba9dec5af68b33e))
|
||||
|
||||
### Features
|
||||
|
||||
- **profile**: Add user self-service profile settings page and API
|
||||
([`b0d6f1a`](https://github.com/christianlouis/DocuElevate/commit/b0d6f1ab609d5676b71eab20fee8c5cce9201614))
|
||||
|
||||
|
||||
## v0.124.0 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **i18n**: Register _ in all Jinja2 template instances, sync translation files, fix pipelines.html
|
||||
([`b483ed1`](https://github.com/christianlouis/DocuElevate/commit/b483ed16fc01e474468356b3af8c2abaa8f22af6))
|
||||
|
||||
|
||||
## v0.123.1 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests**: Fix two failing tests - missing DB table and MagicMock IP address
|
||||
([`be6b49c`](https://github.com/christianlouis/DocuElevate/commit/be6b49c8721445dc8ea870afda87f311acb192e3))
|
||||
|
||||
### Code Style
|
||||
|
||||
- Fix ruff formatting in tests/test_imap_profiles.py
|
||||
([`8060a79`](https://github.com/christianlouis/DocuElevate/commit/8060a79b9c5172c40edff87bdcf0e095e177560e))
|
||||
|
||||
|
||||
## v0.123.0 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **imap**: Address code review feedback on ingestion profiles
|
||||
([`2e08773`](https://github.com/christianlouis/DocuElevate/commit/2e087731b92fe554e54955c97166d61fe644bafc))
|
||||
|
||||
### Features
|
||||
|
||||
- **imap**: Add attachment type filter for IMAP ingestion
|
||||
([`554bb21`](https://github.com/christianlouis/DocuElevate/commit/554bb21d329ca2cca868a61adf337e29e4284ca4))
|
||||
|
||||
- **imap**: Add ImapIngestionProfile model, API, migration and UI
|
||||
([`c9f5544`](https://github.com/christianlouis/DocuElevate/commit/c9f554465d5bf027a92bb4aa56db244aad4a6ff6))
|
||||
|
||||
|
||||
## v0.122.0 (2026-03-12)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **storage**: Document explicit enable/disable flags in ConfigurationGuide and .env.demo
|
||||
([`fd75961`](https://github.com/christianlouis/DocuElevate/commit/fd7596158064b68a23df4a89201f456df8f28b38))
|
||||
|
||||
### Features
|
||||
|
||||
- **storage**: Add explicit enable/disable flag for each global storage destination
|
||||
([`b72ab3b`](https://github.com/christianlouis/DocuElevate/commit/b72ab3b31835ab6ab574067499544427c4101231))
|
||||
|
||||
|
||||
## v0.121.1 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Remove duplicate Audit Logs nav entry and add login/logout audit log events
|
||||
([`1e8f433`](https://github.com/christianlouis/DocuElevate/commit/1e8f433419c5c7b0c551dffe05f107d2bb5a35a1))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`50ef607`](https://github.com/christianlouis/DocuElevate/commit/50ef6072935083554dd52a11cc6438891f177a8b))
|
||||
|
||||
### Testing
|
||||
|
||||
- **notifications**: Improve coverage for user_notification.py to 100%
|
||||
([`78204c2`](https://github.com/christianlouis/DocuElevate/commit/78204c2490ffd9ed0bcb62154c3da54fa9614450))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Testing
|
||||
|
||||
- **notifications**: Improve coverage for user_notification.py to 100%
|
||||
([`78204c2`](https://github.com/christianlouis/DocuElevate/commit/78204c2490ffd9ed0bcb62154c3da54fa9614450))
|
||||
|
||||
|
||||
## v0.121.0 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **auth**: Address code review feedback - sanitize error messages, remove unused import
|
||||
([`5d716ad`](https://github.com/christianlouis/DocuElevate/commit/5d716ad78fdcd92cafa0a7765580ef290cc842fc))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`04cde33`](https://github.com/christianlouis/DocuElevate/commit/04cde33d01d53877fc34a60b887c6978d167a97e))
|
||||
|
||||
### Features
|
||||
|
||||
- **auth**: Add social login support for Google, Microsoft, Apple, and Dropbox
|
||||
([`ac6e052`](https://github.com/christianlouis/DocuElevate/commit/ac6e05278896986ca234e6109e225c599eb982c7))
|
||||
|
||||
### Testing
|
||||
|
||||
- Improve coverage for app/utils/audit_service.py from 59% to 100%
|
||||
([`7438551`](https://github.com/christianlouis/DocuElevate/commit/7438551080dfec645a9e9c9ea8ee059764430e90))
|
||||
|
||||
- **auth**: Add tests for social login and fix existing config validator tests
|
||||
([`9c26d41`](https://github.com/christianlouis/DocuElevate/commit/9c26d412d7710dce47d06969270f184411cd6898))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Testing
|
||||
|
||||
- Improve coverage for app/utils/audit_service.py from 59% to 100%
|
||||
([`7438551`](https://github.com/christianlouis/DocuElevate/commit/7438551080dfec645a9e9c9ea8ee059764430e90))
|
||||
|
||||
|
||||
## v0.120.0 (2026-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Remove accidental pip artifact file and update docs for iCloud Drive
|
||||
([`ca84a11`](https://github.com/christianlouis/DocuElevate/commit/ca84a11284a979409db186b20b0381f5025d4fdf))
|
||||
|
||||
- Remove accidental pip artifact file and update docs for iCloud Drive
|
||||
([`528f0a6`](https://github.com/christianlouis/DocuElevate/commit/528f0a624de335b21125b8b86700eb4d85dfed86))
|
||||
|
||||
- **tests**: Add iCloud mocks to all test files and address code review feedback
|
||||
([`50af0ea`](https://github.com/christianlouis/DocuElevate/commit/50af0ea67942e545849bbd745258cbed7822af45))
|
||||
|
||||
### Features
|
||||
|
||||
- **storage**: Add Apple iCloud Drive storage provider
|
||||
([`82d67c5`](https://github.com/christianlouis/DocuElevate/commit/82d67c56b34ec5a849bb3fa46aba0182c7f51dcd))
|
||||
|
||||
- **storage**: Add Apple iCloud Drive storage provider
|
||||
([`30f06e0`](https://github.com/christianlouis/DocuElevate/commit/30f06e0b3292d9b01428fc945913ab91888644a3))
|
||||
|
||||
### Testing
|
||||
|
||||
- **tasks**: Add _should_upload_to_icloud mock to send_to_all tests
|
||||
([`6e50c61`](https://github.com/christianlouis/DocuElevate/commit/6e50c6197082bbf2de2935cca1b74602789383f4))
|
||||
|
||||
- **tasks**: Add _should_upload_to_icloud mock to send_to_all tests
|
||||
([`9bc23aa`](https://github.com/christianlouis/DocuElevate/commit/9bc23aa40b9c8a9c943cf75f1051fac9e38c24fc))
|
||||
|
||||
|
||||
## v0.119.0 (2026-03-12)
|
||||
|
||||
|
||||
## v0.118.0 (2026-03-11)
|
||||
|
||||
### Features
|
||||
|
||||
- **api**: Add GraphQL endpoint at /graphql with Strawberry
|
||||
([`a41ded5`](https://github.com/christianlouis/DocuElevate/commit/a41ded535f32d4892199208e1cab54cc189fde13))
|
||||
|
||||
|
||||
## v0.117.1 (2026-03-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Resolve all 47 failing tests in main
|
||||
([`df4c91a`](https://github.com/christianlouis/DocuElevate/commit/df4c91a58661e2ecf9fda15500e4ba1259674168))
|
||||
|
||||
|
||||
## v0.117.0 (2026-03-11)
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Opt into Node.js 24 for all GitHub Actions workflows
|
||||
([`c306d80`](https://github.com/christianlouis/DocuElevate/commit/c306d80755193e4b12222c73ed4da442a2d5e23c))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`ab19ae5`](https://github.com/christianlouis/DocuElevate/commit/ab19ae57060ff2be8e9172714c5e3b308c45334d))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`c5b67fe`](https://github.com/christianlouis/DocuElevate/commit/c5b67fe36464b0756553cc54614d0f77273772ec))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`eb22a58`](https://github.com/christianlouis/DocuElevate/commit/eb22a580651b4de477b3b5cc945581776a6775fa))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Opt into Node.js 24 for all GitHub Actions workflows
|
||||
([`c306d80`](https://github.com/christianlouis/DocuElevate/commit/c306d80755193e4b12222c73ed4da442a2d5e23c))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`c5b67fe`](https://github.com/christianlouis/DocuElevate/commit/c5b67fe36464b0756553cc54614d0f77273772ec))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`eb22a58`](https://github.com/christianlouis/DocuElevate/commit/eb22a580651b4de477b3b5cc945581776a6775fa))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Opt into Node.js 24 for all GitHub Actions workflows
|
||||
([`c306d80`](https://github.com/christianlouis/DocuElevate/commit/c306d80755193e4b12222c73ed4da442a2d5e23c))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`eb22a58`](https://github.com/christianlouis/DocuElevate/commit/eb22a580651b4de477b3b5cc945581776a6775fa))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Opt into Node.js 24 for all GitHub Actions workflows
|
||||
([`c306d80`](https://github.com/christianlouis/DocuElevate/commit/c306d80755193e4b12222c73ed4da442a2d5e23c))
|
||||
|
||||
|
||||
## v0.116.0 (2026-03-11)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`d6fb787`](https://github.com/christianlouis/DocuElevate/commit/d6fb78715ae550f25a925bdc629eb2945f455009))
|
||||
|
||||
### Testing
|
||||
|
||||
- **views**: Add comprehensive coverage tests for 11 view modules
|
||||
([`c8987d9`](https://github.com/christianlouis/DocuElevate/commit/c8987d966bd949634d222a7809ec52c3ed258f81))
|
||||
|
||||
- **views**: Boost coverage for 11 view modules toward 100%
|
||||
([`6fb1df1`](https://github.com/christianlouis/DocuElevate/commit/6fb1df1aade8a8c98c27fe50dbe44045cba1724d))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Testing
|
||||
|
||||
- **views**: Add comprehensive coverage tests for 11 view modules
|
||||
([`c8987d9`](https://github.com/christianlouis/DocuElevate/commit/c8987d966bd949634d222a7809ec52c3ed258f81))
|
||||
|
||||
- **views**: Boost coverage for 11 view modules toward 100%
|
||||
([`6fb1df1`](https://github.com/christianlouis/DocuElevate/commit/6fb1df1aade8a8c98c27fe50dbe44045cba1724d))
|
||||
|
||||
|
||||
## v0.115.0 (2026-03-10)
|
||||
|
||||
|
||||
## v0.114.1 (2026-03-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **db**: Add migration to create shared_links table for databases that skipped 025
|
||||
([`289dcc3`](https://github.com/christianlouis/DocuElevate/commit/289dcc375c111c8d71bd04ef31f184a0e6a3f6f2))
|
||||
|
||||
|
||||
## v0.114.0 (2026-03-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
DocuElevate Build Information
|
||||
==============================
|
||||
Version: 0.114.0
|
||||
Build Date: 2026-03-09T23:00:57Z
|
||||
Git Commit: 5fd3f0661b8d86ba6b3f92481675d820aec0d53c
|
||||
Git Short SHA: 5fd3f06
|
||||
Version: 0.125.0
|
||||
Build Date: 2026-03-12T21:44:42Z
|
||||
Git Commit: 4345d5128fe1d33f1342d05138f3d01c903bac0b
|
||||
Git Short SHA: 4345d51
|
||||
Git Branch: main
|
||||
Commit Date: 2026-03-10T00:00:39+01:00
|
||||
Build Timestamp: 2026-03-09T23:00:57Z
|
||||
Commit Date: 2026-03-12T22:44:13+01:00
|
||||
Build Timestamp: 2026-03-12T21:44:42Z
|
||||
==============================
|
||||
|
||||
@@ -8,18 +8,23 @@ from fastapi import APIRouter
|
||||
|
||||
from app.api.admin_users import router as admin_users_router
|
||||
from app.api.api_tokens import router as api_tokens_router
|
||||
from app.api.audit_logs import router as audit_logs_router
|
||||
from app.api.azure import router as azure_router
|
||||
from app.api.backup import router as backup_router
|
||||
from app.api.billing import router as billing_router
|
||||
from app.api.compliance import router as compliance_router
|
||||
from app.api.database import router as database_router
|
||||
from app.api.diagnostic import router as diagnostic_router
|
||||
from app.api.dropbox import router as dropbox_router
|
||||
from app.api.duplicates import router as duplicates_router
|
||||
from app.api.files import router as files_router
|
||||
from app.api.google_drive import router as google_drive_router
|
||||
from app.api.i18n import router as i18n_router
|
||||
from app.api.imap_accounts import router as imap_accounts_router
|
||||
from app.api.imap_profiles import router as imap_profiles_router
|
||||
from app.api.integrations import router as integrations_router
|
||||
from app.api.logs import router as logs_router
|
||||
from app.api.mobile import router as mobile_router
|
||||
from app.api.notifications import router as notifications_router
|
||||
from app.api.onboarding import router as onboarding_router
|
||||
from app.api.onedrive import router as onedrive_router
|
||||
@@ -27,6 +32,7 @@ from app.api.openai import router as openai_router
|
||||
from app.api.pipelines import router as pipelines_router
|
||||
from app.api.plans import router as plans_router
|
||||
from app.api.process import router as process_router
|
||||
from app.api.profile import router as profile_router
|
||||
from app.api.queue import router as queue_router
|
||||
from app.api.routing_rules import router as routing_rules_router
|
||||
from app.api.saved_searches import router as saved_searches_router
|
||||
@@ -79,8 +85,14 @@ router.include_router(plans_router)
|
||||
router.include_router(onboarding_router)
|
||||
router.include_router(billing_router)
|
||||
router.include_router(pipelines_router)
|
||||
router.include_router(profile_router)
|
||||
router.include_router(routing_rules_router)
|
||||
router.include_router(imap_accounts_router)
|
||||
router.include_router(imap_profiles_router)
|
||||
router.include_router(integrations_router)
|
||||
router.include_router(notifications_router)
|
||||
router.include_router(scheduled_jobs_router)
|
||||
router.include_router(audit_logs_router)
|
||||
router.include_router(i18n_router)
|
||||
router.include_router(mobile_router)
|
||||
router.include_router(compliance_router)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Audit log REST API endpoints.
|
||||
|
||||
Provides read-only access to the comprehensive audit log for admin users.
|
||||
Events are append-only — there are no update or delete endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import require_login
|
||||
from app.database import get_db
|
||||
from app.utils.audit_service import count_events, query_events
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/audit-logs")
|
||||
@require_login
|
||||
async def list_audit_logs(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
action: str | None = Query(None, description="Filter by action (exact match)"),
|
||||
user: str | None = Query(None, description="Filter by username"),
|
||||
resource_type: str | None = Query(None, description="Filter by resource type"),
|
||||
severity: str | None = Query(None, description="Filter by severity level"),
|
||||
since: datetime | None = Query(None, description="Only events at or after this ISO-8601 timestamp"),
|
||||
until: datetime | None = Query(None, description="Only events at or before this ISO-8601 timestamp"),
|
||||
limit: int = Query(50, ge=1, le=500, description="Max rows to return"),
|
||||
offset: int = Query(0, ge=0, description="Rows to skip for pagination"),
|
||||
) -> dict[str, Any]:
|
||||
"""Return audit log entries with optional filtering and pagination.
|
||||
|
||||
Requires authentication. Returns events in reverse chronological order.
|
||||
"""
|
||||
entries = query_events(
|
||||
db,
|
||||
action=action,
|
||||
user=user,
|
||||
resource_type=resource_type,
|
||||
severity=severity,
|
||||
since=since,
|
||||
until=until,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
total = count_events(
|
||||
db,
|
||||
action=action,
|
||||
user=user,
|
||||
resource_type=resource_type,
|
||||
severity=severity,
|
||||
since=since,
|
||||
until=until,
|
||||
)
|
||||
return {
|
||||
"items": [_serialize(e) for e in entries],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/audit-logs/actions")
|
||||
@require_login
|
||||
async def list_distinct_actions(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[str]:
|
||||
"""Return the distinct action values present in the audit log."""
|
||||
from app.models import AuditLog
|
||||
|
||||
rows = db.query(AuditLog.action).distinct().order_by(AuditLog.action).all()
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
@router.get("/audit-logs/users")
|
||||
@require_login
|
||||
async def list_distinct_users(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[str]:
|
||||
"""Return the distinct user values present in the audit log."""
|
||||
from app.models import AuditLog
|
||||
|
||||
rows = db.query(AuditLog.user).distinct().order_by(AuditLog.user).all()
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _serialize(entry) -> dict[str, Any]:
|
||||
"""Convert an AuditLog row to a JSON-safe dict."""
|
||||
import json as _json
|
||||
|
||||
return {
|
||||
"id": entry.id,
|
||||
"timestamp": entry.timestamp.isoformat() if entry.timestamp else None,
|
||||
"user": entry.user,
|
||||
"action": entry.action,
|
||||
"resource_type": entry.resource_type,
|
||||
"resource_id": entry.resource_id,
|
||||
"ip_address": entry.ip_address,
|
||||
"details": _json.loads(entry.details) if entry.details else None,
|
||||
"severity": entry.severity,
|
||||
}
|
||||
@@ -30,6 +30,7 @@ from app.auth import require_login
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import SubscriptionPlan, UserProfile
|
||||
from app.utils.i18n import translate as _translate
|
||||
from app.utils.user_scope import get_current_owner_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,6 +38,7 @@ router = APIRouter(prefix="/billing", tags=["billing"])
|
||||
|
||||
_templates_dir = pathlib.Path(__file__).parents[2] / "frontend" / "templates"
|
||||
_templates = Jinja2Templates(directory=str(_templates_dir))
|
||||
_templates.env.globals["_"] = lambda key, **kwargs: _translate(key, "en", **kwargs)
|
||||
|
||||
|
||||
def _get_stripe() -> stripe.StripeClient | None:
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
"""API endpoints for managing compliance templates (GDPR, HIPAA, SOC2).
|
||||
|
||||
All endpoints require admin privileges.
|
||||
|
||||
Available routes:
|
||||
GET /api/compliance/templates – list all compliance templates
|
||||
GET /api/compliance/templates/{name} – get a single template with checks
|
||||
POST /api/compliance/templates/{name}/apply – one-click apply a template
|
||||
GET /api/compliance/templates/{name}/status – evaluate compliance status
|
||||
GET /api/compliance/summary – overall compliance dashboard data
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.utils.compliance_service import (
|
||||
COMPLIANCE_TEMPLATES,
|
||||
apply_template,
|
||||
evaluate_template_status,
|
||||
get_all_templates,
|
||||
get_compliance_summary,
|
||||
get_template_by_name,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/compliance", tags=["compliance"])
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Authorisation helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _require_admin(request: Request) -> dict:
|
||||
"""Ensure the caller is an admin; raises HTTP 403 otherwise."""
|
||||
user = request.session.get("user")
|
||||
if not user or not user.get("is_admin"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
|
||||
return user
|
||||
|
||||
|
||||
AdminUser = Annotated[dict, Depends(_require_admin)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic response models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CheckResult(BaseModel):
|
||||
"""Individual compliance check result."""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
description: str
|
||||
expected: str
|
||||
actual: str
|
||||
passing: bool
|
||||
|
||||
|
||||
class TemplateStatusResponse(BaseModel):
|
||||
"""Status evaluation for a compliance template."""
|
||||
|
||||
status: str
|
||||
total: int
|
||||
passed: int
|
||||
failed: int
|
||||
check_results: list[CheckResult]
|
||||
|
||||
|
||||
class TemplateResponse(BaseModel):
|
||||
"""Full compliance template representation."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None
|
||||
enabled: bool
|
||||
status: str
|
||||
applied_at: str | None
|
||||
applied_by: str | None
|
||||
settings: dict[str, str]
|
||||
checks: list[dict[str, Any]]
|
||||
check_count: int
|
||||
|
||||
|
||||
class ApplyResponse(BaseModel):
|
||||
"""Result of applying a compliance template."""
|
||||
|
||||
success: bool
|
||||
template: str | None = None
|
||||
applied_settings: dict[str, str] | None = None
|
||||
errors: list[str] | None = None
|
||||
error: str | None = None
|
||||
status: TemplateStatusResponse | None = None
|
||||
|
||||
|
||||
class SummaryTemplateResponse(BaseModel):
|
||||
"""Per-template summary for the compliance dashboard."""
|
||||
|
||||
name: str
|
||||
display_name: str
|
||||
enabled: bool
|
||||
status: str
|
||||
total: int
|
||||
passed: int
|
||||
failed: int
|
||||
applied_at: str | None
|
||||
applied_by: str | None
|
||||
|
||||
|
||||
class ComplianceSummaryResponse(BaseModel):
|
||||
"""Overall compliance dashboard summary."""
|
||||
|
||||
overall_status: str
|
||||
total_checks: int
|
||||
total_passed: int
|
||||
total_failed: int
|
||||
templates: list[SummaryTemplateResponse]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/templates", response_model=list[TemplateResponse])
|
||||
async def list_templates(db: DbSession, admin: AdminUser) -> list[dict[str, Any]]:
|
||||
"""List all compliance templates with their current status."""
|
||||
return get_all_templates(db)
|
||||
|
||||
|
||||
@router.get("/templates/{name}", response_model=TemplateResponse)
|
||||
async def get_template(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||||
"""Get a single compliance template by name."""
|
||||
templates = get_all_templates(db)
|
||||
for t in templates:
|
||||
if t["name"] == name:
|
||||
return t
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||||
|
||||
|
||||
@router.post("/templates/{name}/apply", response_model=ApplyResponse)
|
||||
async def apply_compliance_template(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||||
"""Apply a compliance template (one-click).
|
||||
|
||||
Writes all template settings to the database and evaluates the resulting
|
||||
compliance status.
|
||||
"""
|
||||
if name not in COMPLIANCE_TEMPLATES:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||||
|
||||
template = get_template_by_name(db, name)
|
||||
if template is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||||
|
||||
admin_email = admin.get("email", "admin")
|
||||
result = apply_template(db, name, applied_by=admin_email)
|
||||
if not result.get("success") and result.get("error"):
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result["error"])
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/templates/{name}/status", response_model=TemplateStatusResponse)
|
||||
async def get_template_status(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||||
"""Evaluate the live compliance status of a template."""
|
||||
template = get_template_by_name(db, name)
|
||||
if template is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||||
return evaluate_template_status(db, name)
|
||||
|
||||
|
||||
@router.get("/summary", response_model=ComplianceSummaryResponse)
|
||||
async def compliance_summary(db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||||
"""Overall compliance dashboard summary across all templates."""
|
||||
return get_compliance_summary(db)
|
||||
@@ -0,0 +1,431 @@
|
||||
"""
|
||||
GraphQL API endpoint for DocuElevate.
|
||||
|
||||
Provides a flexible query interface alongside the existing REST API.
|
||||
Schema covers: documents, pipelines, settings, and users.
|
||||
|
||||
Endpoint: /graphql
|
||||
GraphiQL playground: /graphql (via browser)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any
|
||||
|
||||
import strawberry
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
|
||||
from app.auth import get_current_user
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import ApplicationSettings, FileRecord, Pipeline, PipelineStep, UserProfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strawberry types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class DocumentType:
|
||||
"""A processed document stored in the system."""
|
||||
|
||||
id: int
|
||||
owner_id: str | None
|
||||
original_filename: str | None
|
||||
local_filename: str
|
||||
file_size: int
|
||||
mime_type: str | None
|
||||
document_title: str | None
|
||||
is_duplicate: bool
|
||||
ocr_quality_score: int | None
|
||||
pipeline_id: int | None
|
||||
created_at: datetime | None
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PipelineStepType:
|
||||
"""A single step within a processing pipeline."""
|
||||
|
||||
id: int
|
||||
pipeline_id: int
|
||||
position: int
|
||||
step_type: str
|
||||
label: str | None
|
||||
enabled: bool
|
||||
created_at: datetime | None
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PipelineType:
|
||||
"""A processing pipeline with its ordered steps."""
|
||||
|
||||
id: int
|
||||
owner_id: str | None
|
||||
name: str
|
||||
description: str | None
|
||||
is_default: bool
|
||||
is_active: bool
|
||||
steps: list[PipelineStepType]
|
||||
created_at: datetime | None
|
||||
updated_at: datetime | None
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class SettingType:
|
||||
"""An application configuration setting stored in the database."""
|
||||
|
||||
id: int
|
||||
key: str
|
||||
value: str | None
|
||||
created_at: datetime | None
|
||||
updated_at: datetime | None
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class UserType:
|
||||
"""A user profile in the system."""
|
||||
|
||||
id: int
|
||||
user_id: str
|
||||
display_name: str | None
|
||||
is_blocked: bool
|
||||
subscription_tier: str | None
|
||||
onboarding_completed: bool
|
||||
created_at: datetime | None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conversion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _document_from_record(rec: FileRecord) -> DocumentType:
|
||||
return DocumentType(
|
||||
id=rec.id,
|
||||
owner_id=rec.owner_id,
|
||||
original_filename=rec.original_filename,
|
||||
local_filename=rec.local_filename,
|
||||
file_size=rec.file_size,
|
||||
mime_type=rec.mime_type,
|
||||
document_title=rec.document_title,
|
||||
is_duplicate=rec.is_duplicate,
|
||||
ocr_quality_score=rec.ocr_quality_score,
|
||||
pipeline_id=rec.pipeline_id,
|
||||
created_at=rec.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _pipeline_step_from_record(step: PipelineStep) -> PipelineStepType:
|
||||
return PipelineStepType(
|
||||
id=step.id,
|
||||
pipeline_id=step.pipeline_id,
|
||||
position=step.position,
|
||||
step_type=step.step_type,
|
||||
label=step.label,
|
||||
enabled=step.enabled,
|
||||
created_at=step.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _pipeline_from_record(pipeline: Pipeline, db: Session) -> PipelineType:
|
||||
steps = db.query(PipelineStep).filter(PipelineStep.pipeline_id == pipeline.id).order_by(PipelineStep.position).all()
|
||||
return PipelineType(
|
||||
id=pipeline.id,
|
||||
owner_id=pipeline.owner_id,
|
||||
name=pipeline.name,
|
||||
description=pipeline.description,
|
||||
is_default=pipeline.is_default,
|
||||
is_active=pipeline.is_active,
|
||||
steps=[_pipeline_step_from_record(s) for s in steps],
|
||||
created_at=pipeline.created_at,
|
||||
updated_at=pipeline.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _setting_from_record(setting: ApplicationSettings) -> SettingType:
|
||||
return SettingType(
|
||||
id=setting.id,
|
||||
key=setting.key,
|
||||
value=setting.value,
|
||||
created_at=setting.created_at,
|
||||
updated_at=setting.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _user_from_profile(profile: UserProfile) -> UserType:
|
||||
return UserType(
|
||||
id=profile.id,
|
||||
user_id=profile.user_id,
|
||||
display_name=profile.display_name,
|
||||
is_blocked=profile.is_blocked,
|
||||
subscription_tier=profile.subscription_tier,
|
||||
onboarding_completed=profile.onboarding_completed,
|
||||
created_at=profile.created_at,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Keys that contain sensitive data and must never be returned via GraphQL
|
||||
_SENSITIVE_SETTING_KEYS: frozenset[str] = frozenset(
|
||||
{
|
||||
"openai_api_key",
|
||||
"azure_ai_key",
|
||||
"session_secret",
|
||||
"database_url",
|
||||
"redis_url",
|
||||
"dropbox_app_secret",
|
||||
"dropbox_refresh_token",
|
||||
"google_drive_credentials_json",
|
||||
"onedrive_client_secret",
|
||||
"onedrive_refresh_token",
|
||||
"smtp_password",
|
||||
"nextcloud_password",
|
||||
"s3_secret_access_key",
|
||||
"ftp_password",
|
||||
"sftp_password",
|
||||
"webdav_password",
|
||||
"stripe_secret_key",
|
||||
"stripe_webhook_secret",
|
||||
"sentry_dsn",
|
||||
"social_auth_google_client_secret",
|
||||
"social_auth_microsoft_client_secret",
|
||||
"social_auth_apple_private_key",
|
||||
"social_auth_dropbox_app_secret",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_current_user_id(user: dict[str, Any] | None) -> str | None:
|
||||
"""Extract the stable user identifier from the user dict."""
|
||||
if not user:
|
||||
return None
|
||||
return user.get("preferred_username") or user.get("email") or user.get("id") or None
|
||||
|
||||
|
||||
def _get_db_and_user(info: strawberry.types.Info) -> tuple[Session, dict[str, Any] | None]:
|
||||
"""Extract the database session and current user from the Strawberry context."""
|
||||
db: Session = info.context["db"]
|
||||
user: dict[str, Any] | None = info.context.get("user")
|
||||
return db, user
|
||||
|
||||
|
||||
def _require_auth(user: dict[str, Any] | None) -> None:
|
||||
"""Raise an error when authentication is enabled and no valid user is present."""
|
||||
if settings.auth_enabled and not user:
|
||||
raise strawberry.exceptions.StrawberryGraphQLError("Authentication required")
|
||||
|
||||
|
||||
def _require_admin(user: dict[str, Any] | None) -> None:
|
||||
"""Raise an error when the current user is not an admin.
|
||||
|
||||
When ``auth_enabled`` is *False* (single-user / development mode) all
|
||||
callers are implicitly treated as administrators.
|
||||
"""
|
||||
if not settings.auth_enabled:
|
||||
# Single-user mode: no auth, treat caller as admin
|
||||
return
|
||||
_require_auth(user)
|
||||
if not (user and user.get("is_admin")):
|
||||
raise strawberry.exceptions.StrawberryGraphQLError("Admin access required")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Query resolvers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Query:
|
||||
"""Root query type for the DocuElevate GraphQL API."""
|
||||
|
||||
@strawberry.field(description="List documents, optionally filtered by owner.")
|
||||
def documents(
|
||||
self,
|
||||
info: strawberry.types.Info,
|
||||
owner_id: str | None = None,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> list[DocumentType]:
|
||||
"""Return a paginated list of documents.
|
||||
|
||||
When *auth_enabled* the caller must be authenticated. Non-admin users
|
||||
receive only their own documents; admins may query any *owner_id*.
|
||||
"""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_auth(user)
|
||||
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
|
||||
query = db.query(FileRecord)
|
||||
|
||||
if settings.auth_enabled and user:
|
||||
is_admin = user.get("is_admin", False)
|
||||
current_user_id = _get_current_user_id(user)
|
||||
if not is_admin:
|
||||
# Non-admins can only see their own documents
|
||||
query = query.filter(FileRecord.owner_id == current_user_id)
|
||||
elif owner_id:
|
||||
query = query.filter(FileRecord.owner_id == owner_id)
|
||||
elif owner_id:
|
||||
query = query.filter(FileRecord.owner_id == owner_id)
|
||||
|
||||
records = query.order_by(FileRecord.created_at.desc()).offset(offset).limit(limit).all()
|
||||
return [_document_from_record(r) for r in records]
|
||||
|
||||
@strawberry.field(description="Fetch a single document by ID.")
|
||||
def document(self, info: strawberry.types.Info, id: int) -> DocumentType | None:
|
||||
"""Return one document by its primary key, or *null* if not found."""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_auth(user)
|
||||
|
||||
rec = db.query(FileRecord).filter(FileRecord.id == id).first()
|
||||
if rec is None:
|
||||
return None
|
||||
|
||||
if settings.auth_enabled and user:
|
||||
is_admin = user.get("is_admin", False)
|
||||
current_user_id = _get_current_user_id(user)
|
||||
if not is_admin and rec.owner_id != current_user_id:
|
||||
return None
|
||||
|
||||
return _document_from_record(rec)
|
||||
|
||||
@strawberry.field(description="List processing pipelines.")
|
||||
def pipelines(
|
||||
self,
|
||||
info: strawberry.types.Info,
|
||||
owner_id: str | None = None,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> list[PipelineType]:
|
||||
"""Return a paginated list of pipelines."""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_auth(user)
|
||||
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
|
||||
query = db.query(Pipeline)
|
||||
|
||||
if settings.auth_enabled and user:
|
||||
is_admin = user.get("is_admin", False)
|
||||
current_user_id = _get_current_user_id(user)
|
||||
if not is_admin:
|
||||
query = query.filter((Pipeline.owner_id == current_user_id) | (Pipeline.owner_id.is_(None)))
|
||||
elif owner_id:
|
||||
query = query.filter(Pipeline.owner_id == owner_id)
|
||||
elif owner_id:
|
||||
query = query.filter(Pipeline.owner_id == owner_id)
|
||||
|
||||
rows = query.order_by(Pipeline.id).offset(offset).limit(limit).all()
|
||||
return [_pipeline_from_record(p, db) for p in rows]
|
||||
|
||||
@strawberry.field(description="Fetch a single pipeline by ID.")
|
||||
def pipeline(self, info: strawberry.types.Info, id: int) -> PipelineType | None:
|
||||
"""Return one pipeline by its primary key, or *null* if not found."""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_auth(user)
|
||||
|
||||
row = db.query(Pipeline).filter(Pipeline.id == id).first()
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
if settings.auth_enabled and user:
|
||||
is_admin = user.get("is_admin", False)
|
||||
current_user_id = _get_current_user_id(user)
|
||||
if not is_admin and row.owner_id is not None and row.owner_id != current_user_id:
|
||||
return None
|
||||
|
||||
return _pipeline_from_record(row, db)
|
||||
|
||||
@strawberry.field(description="List non-sensitive application settings (admin only).")
|
||||
def settings(
|
||||
self,
|
||||
info: strawberry.types.Info,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> list[SettingType]:
|
||||
"""Return application settings stored in the database.
|
||||
|
||||
Sensitive keys (API secrets, passwords, etc.) are automatically
|
||||
excluded. Requires admin privileges when auth is enabled.
|
||||
"""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_admin(user)
|
||||
|
||||
limit = max(1, min(limit, 200))
|
||||
offset = max(0, offset)
|
||||
|
||||
rows = (
|
||||
db.query(ApplicationSettings)
|
||||
.filter(ApplicationSettings.key.notin_(_SENSITIVE_SETTING_KEYS))
|
||||
.order_by(ApplicationSettings.key)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [_setting_from_record(r) for r in rows]
|
||||
|
||||
@strawberry.field(description="List user profiles (admin only).")
|
||||
def users(
|
||||
self,
|
||||
info: strawberry.types.Info,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> list[UserType]:
|
||||
"""Return a paginated list of user profiles. Requires admin privileges."""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_admin(user)
|
||||
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
|
||||
rows = db.query(UserProfile).order_by(UserProfile.user_id).offset(offset).limit(limit).all()
|
||||
return [_user_from_profile(r) for r in rows]
|
||||
|
||||
@strawberry.field(description="Fetch a user profile by user_id (admin only).")
|
||||
def user(self, info: strawberry.types.Info, user_id: str) -> UserType | None:
|
||||
"""Return one user profile by *user_id*, or *null* if not found."""
|
||||
db, user = _get_db_and_user(info)
|
||||
_require_admin(user)
|
||||
|
||||
row = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
return _user_from_profile(row) if row else None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema and router
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
schema = strawberry.Schema(query=Query)
|
||||
|
||||
|
||||
async def get_graphql_context(
|
||||
request: Request,
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
) -> dict[str, Any]:
|
||||
"""Build the per-request context injected into every resolver."""
|
||||
try:
|
||||
user = get_current_user(request)
|
||||
except Exception:
|
||||
logger.debug("Could not resolve current user for GraphQL context", exc_info=True)
|
||||
user = None
|
||||
return {"request": request, "db": db, "user": user}
|
||||
|
||||
|
||||
graphql_router = GraphQLRouter(
|
||||
schema,
|
||||
context_getter=get_graphql_context,
|
||||
graphql_ide="graphiql",
|
||||
)
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
"""API endpoints for internationalization (i18n).
|
||||
|
||||
Provides endpoints for:
|
||||
* Listing available languages
|
||||
* Getting/setting user language preference (persisted in session + cookie + DB)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import UserProfile
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
detect_language,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/i18n", tags=["i18n"])
|
||||
|
||||
|
||||
class LanguageInfo(BaseModel):
|
||||
"""Schema for a supported language."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
native: str
|
||||
flag: str
|
||||
|
||||
|
||||
class LanguageListResponse(BaseModel):
|
||||
"""Response for the list-languages endpoint."""
|
||||
|
||||
languages: list[LanguageInfo]
|
||||
current: str
|
||||
default: str
|
||||
|
||||
|
||||
class SetLanguageRequest(BaseModel):
|
||||
"""Request body for setting the preferred language."""
|
||||
|
||||
language: str
|
||||
|
||||
|
||||
class SetLanguageResponse(BaseModel):
|
||||
"""Response after changing the language."""
|
||||
|
||||
language: str
|
||||
message: str
|
||||
|
||||
|
||||
@router.get("/languages", response_model=LanguageListResponse)
|
||||
async def list_languages(request: Request) -> LanguageListResponse:
|
||||
"""Return all supported UI languages and the current active language."""
|
||||
current = detect_language(request)
|
||||
return LanguageListResponse(
|
||||
languages=[LanguageInfo(**lang) for lang in SUPPORTED_LANGUAGES],
|
||||
current=current,
|
||||
default=DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/language", response_model=SetLanguageResponse)
|
||||
async def set_language(
|
||||
body: SetLanguageRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SetLanguageResponse:
|
||||
"""Set the preferred UI language.
|
||||
|
||||
Persists the choice in:
|
||||
1. The server-side session
|
||||
2. A ``docuelevate_lang`` cookie (30-day expiry)
|
||||
3. The ``UserProfile.preferred_language`` column (if authenticated)
|
||||
"""
|
||||
lang = body.language.lower().strip()
|
||||
if lang not in SUPPORTED_LANGUAGE_CODES:
|
||||
lang = DEFAULT_LANGUAGE
|
||||
|
||||
# 1. Session
|
||||
if hasattr(request, "session"):
|
||||
request.session["preferred_language"] = lang
|
||||
|
||||
# 2. Cookie (30 days)
|
||||
response.set_cookie(
|
||||
key="docuelevate_lang",
|
||||
value=lang,
|
||||
max_age=30 * 24 * 60 * 60,
|
||||
httponly=False,
|
||||
samesite="lax",
|
||||
)
|
||||
|
||||
# 3. Database (if user is authenticated)
|
||||
_persist_language_to_profile(request, db, lang)
|
||||
|
||||
language_name = next(
|
||||
(entry["native"] for entry in SUPPORTED_LANGUAGES if entry["code"] == lang),
|
||||
lang,
|
||||
)
|
||||
logger.info("Language preference set to '%s'", lang)
|
||||
return SetLanguageResponse(
|
||||
language=lang,
|
||||
message=f"Language changed to {language_name}",
|
||||
)
|
||||
|
||||
|
||||
def _persist_language_to_profile(request: Request, db: Session, lang: str) -> None:
|
||||
"""Write language preference to the UserProfile row, if the user is logged in."""
|
||||
user_id: str | None = None
|
||||
if hasattr(request, "session"):
|
||||
user = request.session.get("user")
|
||||
if isinstance(user, dict):
|
||||
user_id = user.get("preferred_username") or user.get("email") or user.get("id")
|
||||
elif isinstance(user, str):
|
||||
user_id = user
|
||||
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
try:
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
if profile:
|
||||
profile.preferred_language = lang # type: ignore[attr-defined]
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.debug("Could not persist language preference for user_id=%s", user_id)
|
||||
@@ -110,6 +110,13 @@ class ImapAccountCreate(BaseModel):
|
||||
use_ssl: bool = Field(default=True, description="Use SSL/TLS connection")
|
||||
delete_after_process: bool = Field(default=False, description="Delete emails from mailbox after processing")
|
||||
is_active: bool = Field(default=True, description="Whether to poll this mailbox")
|
||||
profile_id: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"ID of the ImapIngestionProfile that controls which attachment types to ingest. "
|
||||
"Null inherits the global imap_attachment_filter setting."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ImapAccountUpdate(BaseModel):
|
||||
@@ -123,6 +130,13 @@ class ImapAccountUpdate(BaseModel):
|
||||
use_ssl: bool | None = None
|
||||
delete_after_process: bool | None = None
|
||||
is_active: bool | None = None
|
||||
profile_id: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"ID of the ImapIngestionProfile to use. "
|
||||
"Explicitly sending null clears the override (falls back to global setting)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ImapTestRequest(BaseModel):
|
||||
@@ -155,6 +169,7 @@ def _to_response(acct: UserImapAccount) -> dict[str, Any]:
|
||||
"use_ssl": acct.use_ssl,
|
||||
"delete_after_process": acct.delete_after_process,
|
||||
"is_active": acct.is_active,
|
||||
"profile_id": acct.profile_id,
|
||||
"last_checked_at": acct.last_checked_at.isoformat() if acct.last_checked_at else None,
|
||||
"last_error": acct.last_error,
|
||||
"created_at": acct.created_at.isoformat() if acct.created_at else None,
|
||||
@@ -222,6 +237,7 @@ def create_imap_account(
|
||||
use_ssl=body.use_ssl,
|
||||
delete_after_process=body.delete_after_process,
|
||||
is_active=body.is_active,
|
||||
profile_id=body.profile_id,
|
||||
)
|
||||
try:
|
||||
db.add(acct)
|
||||
@@ -277,6 +293,10 @@ def update_imap_account(
|
||||
acct.delete_after_process = body.delete_after_process
|
||||
if body.is_active is not None:
|
||||
acct.is_active = body.is_active
|
||||
# profile_id: update whenever the field is explicitly present in the request payload
|
||||
# (including sending null to clear the override).
|
||||
if "profile_id" in body.model_fields_set:
|
||||
acct.profile_id = body.profile_id
|
||||
|
||||
# Reset last_error so the next poll gives a fresh result
|
||||
acct.last_error = None
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
"""API endpoints for managing IMAP ingestion profiles.
|
||||
|
||||
Ingestion profiles allow fine-grained control over which attachment types are
|
||||
accepted when ingesting emails via IMAP. Each profile carries a list of enabled
|
||||
file-type categories (e.g. ``["pdf", "office", "images"]``) drawn from the
|
||||
canonical set defined in :mod:`app.utils.allowed_types`.
|
||||
|
||||
Built-in system profiles (``is_builtin=True``) are read-only and cannot be
|
||||
deleted or modified. Users may create their own profiles which are private to
|
||||
their ``owner_id``. System-level global profiles (``owner_id=None``) are visible
|
||||
to all users but can only be created by administrators.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import ImapIngestionProfile
|
||||
from app.utils.allowed_types import FILE_TYPE_CATEGORIES
|
||||
from app.utils.user_scope import get_current_owner_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/imap-profiles", tags=["imap-profiles"])
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_owner_id(request: Request) -> str:
|
||||
"""Return the current user's owner ID, raising 401 if unauthenticated."""
|
||||
owner_id = get_current_owner_id(request)
|
||||
if owner_id is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
return owner_id
|
||||
|
||||
|
||||
CurrentOwner = Annotated[str, Depends(_get_owner_id)]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VALID_CATEGORIES = set(FILE_TYPE_CATEGORIES.keys())
|
||||
|
||||
|
||||
class ImapProfileCreate(BaseModel):
|
||||
"""Schema for creating a new ingestion profile."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255, description="Human-readable profile name")
|
||||
description: str | None = Field(default=None, description="Optional description")
|
||||
allowed_categories: list[str] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description=(f"List of enabled file-type category keys. Valid values: {sorted(_VALID_CATEGORIES)}"),
|
||||
)
|
||||
|
||||
|
||||
class ImapProfileUpdate(BaseModel):
|
||||
"""Schema for updating an existing profile (all fields optional)."""
|
||||
|
||||
name: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
description: str | None = None
|
||||
allowed_categories: list[str] | None = Field(default=None, min_length=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _validate_categories(categories: list[str]) -> list[str]:
|
||||
"""Raise 422 if any category key is unknown; return the cleaned list."""
|
||||
unknown = [c for c in categories if c not in _VALID_CATEGORIES]
|
||||
if unknown:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Unknown category key(s): {unknown}. Valid keys: {sorted(_VALID_CATEGORIES)}",
|
||||
)
|
||||
# Deduplicate while preserving order
|
||||
seen: set[str] = set()
|
||||
result: list[str] = []
|
||||
for cat in categories:
|
||||
if cat not in seen:
|
||||
seen.add(cat)
|
||||
result.append(cat)
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialisation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _to_response(profile: ImapIngestionProfile) -> dict[str, Any]:
|
||||
"""Serialize a profile row to a response dict."""
|
||||
try:
|
||||
categories = json.loads(profile.allowed_categories)
|
||||
except (ValueError, TypeError):
|
||||
categories = []
|
||||
|
||||
# Enrich categories with display metadata
|
||||
categories_detail = [
|
||||
{
|
||||
"key": cat,
|
||||
"label": FILE_TYPE_CATEGORIES[cat]["label"] if cat in FILE_TYPE_CATEGORIES else cat,
|
||||
"description": FILE_TYPE_CATEGORIES[cat]["description"] if cat in FILE_TYPE_CATEGORIES else "",
|
||||
}
|
||||
for cat in categories
|
||||
]
|
||||
|
||||
return {
|
||||
"id": profile.id,
|
||||
"name": profile.name,
|
||||
"description": profile.description,
|
||||
"owner_id": profile.owner_id,
|
||||
"allowed_categories": categories,
|
||||
"categories_detail": categories_detail,
|
||||
"is_builtin": profile.is_builtin,
|
||||
"created_at": profile.created_at.isoformat() if profile.created_at else None,
|
||||
"updated_at": profile.updated_at.isoformat() if profile.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/categories", summary="List available file-type categories")
|
||||
def list_categories(request: Request, owner_id: CurrentOwner) -> list[dict[str, Any]]:
|
||||
"""Return the full list of file-type categories that can be used in profiles."""
|
||||
return [
|
||||
{
|
||||
"key": key,
|
||||
"label": info["label"],
|
||||
"description": info["description"],
|
||||
}
|
||||
for key, info in FILE_TYPE_CATEGORIES.items()
|
||||
]
|
||||
|
||||
|
||||
@router.get("/", summary="List ingestion profiles visible to the current user")
|
||||
def list_profiles(request: Request, db: DbSession, owner_id: CurrentOwner) -> list[dict[str, Any]]:
|
||||
"""Return all profiles: system-global (owner_id=NULL) and the user's own profiles."""
|
||||
profiles = (
|
||||
db.query(ImapIngestionProfile)
|
||||
.filter(
|
||||
# SQLAlchemy requires `== None` for IS NULL comparison in ORM filters
|
||||
(ImapIngestionProfile.owner_id == None) | (ImapIngestionProfile.owner_id == owner_id) # noqa: E711
|
||||
)
|
||||
.order_by(ImapIngestionProfile.is_builtin.desc(), ImapIngestionProfile.id)
|
||||
.all()
|
||||
)
|
||||
return [_to_response(p) for p in profiles]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED, summary="Create a new ingestion profile")
|
||||
def create_profile(request: Request, body: ImapProfileCreate, db: DbSession, owner_id: CurrentOwner) -> dict[str, Any]:
|
||||
"""Create a new ingestion profile owned by the current user."""
|
||||
categories = _validate_categories(body.allowed_categories)
|
||||
|
||||
profile = ImapIngestionProfile(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
owner_id=owner_id,
|
||||
allowed_categories=json.dumps(categories),
|
||||
is_builtin=False,
|
||||
)
|
||||
try:
|
||||
db.add(profile)
|
||||
db.commit()
|
||||
db.refresh(profile)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
logger.info("User %s created IMAP ingestion profile %d ('%s')", owner_id, profile.id, body.name)
|
||||
return _to_response(profile)
|
||||
|
||||
|
||||
@router.get("/{profile_id}", summary="Get a single ingestion profile")
|
||||
def get_profile(profile_id: int, request: Request, db: DbSession, owner_id: CurrentOwner) -> dict[str, Any]:
|
||||
"""Return a single profile by ID. Only the owner or system profiles are accessible."""
|
||||
profile = db.query(ImapIngestionProfile).filter(ImapIngestionProfile.id == profile_id).first()
|
||||
if not profile or (profile.owner_id is not None and profile.owner_id != owner_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ingestion profile not found")
|
||||
return _to_response(profile)
|
||||
|
||||
|
||||
@router.put("/{profile_id}", summary="Update an ingestion profile")
|
||||
def update_profile(
|
||||
profile_id: int,
|
||||
request: Request,
|
||||
body: ImapProfileUpdate,
|
||||
db: DbSession,
|
||||
owner_id: CurrentOwner,
|
||||
) -> dict[str, Any]:
|
||||
"""Update an existing ingestion profile. Built-in profiles cannot be modified."""
|
||||
profile = db.query(ImapIngestionProfile).filter(ImapIngestionProfile.id == profile_id).first()
|
||||
if not profile or (profile.owner_id is not None and profile.owner_id != owner_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ingestion profile not found")
|
||||
if profile.is_builtin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Built-in profiles cannot be modified.",
|
||||
)
|
||||
|
||||
if body.name is not None:
|
||||
profile.name = body.name
|
||||
if "description" in body.model_fields_set:
|
||||
profile.description = body.description
|
||||
if body.allowed_categories is not None:
|
||||
categories = _validate_categories(body.allowed_categories)
|
||||
profile.allowed_categories = json.dumps(categories)
|
||||
|
||||
profile.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(profile)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
logger.info("User %s updated IMAP ingestion profile %d", owner_id, profile_id)
|
||||
return _to_response(profile)
|
||||
|
||||
|
||||
@router.delete("/{profile_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete an ingestion profile")
|
||||
def delete_profile(profile_id: int, request: Request, db: DbSession, owner_id: CurrentOwner) -> None:
|
||||
"""Delete an ingestion profile. Built-in profiles cannot be deleted."""
|
||||
profile = db.query(ImapIngestionProfile).filter(ImapIngestionProfile.id == profile_id).first()
|
||||
if not profile or (profile.owner_id is not None and profile.owner_id != owner_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ingestion profile not found")
|
||||
if profile.is_builtin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Built-in profiles cannot be deleted.",
|
||||
)
|
||||
|
||||
try:
|
||||
db.delete(profile)
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
logger.info("User %s deleted IMAP ingestion profile %d", owner_id, profile_id)
|
||||
@@ -26,6 +26,7 @@ from starlette.responses import RedirectResponse
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import LocalUser, UserProfile
|
||||
from app.utils.i18n import translate as _translate
|
||||
from app.utils.local_auth import (
|
||||
build_session_user,
|
||||
generate_token,
|
||||
@@ -41,6 +42,7 @@ router = APIRouter(tags=["local-auth"])
|
||||
|
||||
_templates_dir = pathlib.Path(__file__).parents[2] / "frontend" / "templates"
|
||||
templates = Jinja2Templates(directory=str(_templates_dir))
|
||||
templates.env.globals["_"] = lambda key, **kwargs: _translate(key, "en", **kwargs)
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
"""Mobile app API endpoints.
|
||||
|
||||
Provides endpoints specifically designed for the DocuElevate native mobile
|
||||
app (iOS / Android via React Native / Expo):
|
||||
|
||||
* ``POST /mobile/generate-token`` – exchange an active session for a
|
||||
long-lived API token that the mobile app stores securely. The token is
|
||||
auto-named "Mobile App – <device_name>" and is identical to regular API
|
||||
tokens (Bearer auth works everywhere).
|
||||
|
||||
* ``POST /mobile/register-device`` – register a push-notification device
|
||||
token (Expo push token) so the user receives push notifications when
|
||||
documents finish processing.
|
||||
|
||||
* ``GET /mobile/devices`` – list registered devices for the current user.
|
||||
|
||||
* ``DELETE /mobile/devices/{device_id}`` – deactivate a device.
|
||||
|
||||
* ``GET /mobile/whoami`` – lightweight profile endpoint for the mobile app
|
||||
to verify authentication state.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.api_tokens import generate_api_token, hash_token
|
||||
from app.auth import require_login
|
||||
from app.database import get_db
|
||||
from app.models import ApiToken, MobileDevice
|
||||
from app.utils.user_scope import get_current_owner_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/mobile", tags=["mobile"])
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_owner_id(request: Request) -> str:
|
||||
"""Return the current user's owner ID, raising 401 if unauthenticated."""
|
||||
owner_id = get_current_owner_id(request)
|
||||
if not owner_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
return owner_id
|
||||
|
||||
|
||||
CurrentOwner = Annotated[str, Depends(_get_owner_id)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request / Response schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GenerateTokenRequest(BaseModel):
|
||||
"""Request body for auto-generating a mobile app token."""
|
||||
|
||||
device_name: str = Field(
|
||||
default="Mobile App",
|
||||
min_length=1,
|
||||
max_length=120,
|
||||
description="Human-readable device name used to label the token.",
|
||||
)
|
||||
|
||||
|
||||
class GenerateTokenResponse(BaseModel):
|
||||
"""Response containing the one-time-visible API token."""
|
||||
|
||||
token: str
|
||||
token_id: int
|
||||
name: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class RegisterDeviceRequest(BaseModel):
|
||||
"""Request body for registering a push-notification device token."""
|
||||
|
||||
push_token: str = Field(
|
||||
min_length=1,
|
||||
max_length=512,
|
||||
description="Expo push token (ExponentPushToken[…]) obtained from the mobile app.",
|
||||
)
|
||||
device_name: str | None = Field(
|
||||
default=None,
|
||||
max_length=255,
|
||||
description="Optional human-readable device name (e.g. 'John's iPhone').",
|
||||
)
|
||||
platform: str = Field(
|
||||
default="ios",
|
||||
description="Device platform: 'ios', 'android', or 'web'.",
|
||||
)
|
||||
|
||||
|
||||
class DeviceResponse(BaseModel):
|
||||
"""Serialised MobileDevice record."""
|
||||
|
||||
id: int
|
||||
device_name: str | None
|
||||
platform: str
|
||||
push_token_preview: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
last_seen_at: datetime | None
|
||||
|
||||
|
||||
class WhoAmIResponse(BaseModel):
|
||||
"""Lightweight profile response for the mobile app."""
|
||||
|
||||
owner_id: str
|
||||
display_name: str | None
|
||||
email: str | None
|
||||
avatar_url: str | None
|
||||
is_admin: bool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _device_to_response(device: MobileDevice) -> dict[str, Any]:
|
||||
"""Convert a MobileDevice ORM object to a serialisable dict."""
|
||||
# Show only first 20 chars of the push token for security.
|
||||
token_preview = device.push_token[:20] + "…" if len(device.push_token) > 20 else device.push_token
|
||||
return {
|
||||
"id": device.id,
|
||||
"device_name": device.device_name,
|
||||
"platform": device.platform,
|
||||
"push_token_preview": token_preview,
|
||||
"is_active": device.is_active,
|
||||
"created_at": device.created_at,
|
||||
"last_seen_at": device.last_seen_at,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/generate-token", status_code=status.HTTP_201_CREATED, response_model=GenerateTokenResponse)
|
||||
@require_login
|
||||
async def generate_mobile_token(
|
||||
request: Request,
|
||||
body: GenerateTokenRequest,
|
||||
owner_id: CurrentOwner,
|
||||
db: DbSession,
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a long-lived API token for the mobile app.
|
||||
|
||||
The mobile app calls this endpoint immediately after SSO login to obtain
|
||||
a Bearer token it can store in the secure keychain. The returned token
|
||||
is functionally identical to manually-created API tokens and works with
|
||||
every authenticated endpoint.
|
||||
|
||||
The token is shown **exactly once** in the response; subsequent requests
|
||||
show only the prefix for identification.
|
||||
"""
|
||||
token_name = f"Mobile App – {body.device_name}"
|
||||
plaintext = generate_api_token()
|
||||
token_hash_value = hash_token(plaintext)
|
||||
prefix = plaintext[:12]
|
||||
|
||||
db_token = ApiToken(
|
||||
owner_id=owner_id,
|
||||
name=token_name,
|
||||
token_hash=token_hash_value,
|
||||
token_prefix=prefix,
|
||||
)
|
||||
try:
|
||||
db.add(db_token)
|
||||
db.commit()
|
||||
db.refresh(db_token)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.exception("Failed to create mobile API token for owner_id=%s", owner_id)
|
||||
raise
|
||||
|
||||
logger.info("Mobile API token created: id=%s owner=%s device=%r", db_token.id, owner_id, body.device_name)
|
||||
|
||||
return {
|
||||
"token": plaintext,
|
||||
"token_id": db_token.id,
|
||||
"name": token_name,
|
||||
"created_at": db_token.created_at,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/register-device", status_code=status.HTTP_201_CREATED, response_model=DeviceResponse)
|
||||
@require_login
|
||||
async def register_device(
|
||||
request: Request,
|
||||
body: RegisterDeviceRequest,
|
||||
owner_id: CurrentOwner,
|
||||
db: DbSession,
|
||||
) -> dict[str, Any]:
|
||||
"""Register or refresh a push-notification device token.
|
||||
|
||||
If the same ``push_token`` is already registered for this user the
|
||||
record is reactivated and ``last_seen_at`` is updated rather than
|
||||
creating a duplicate.
|
||||
"""
|
||||
platform = body.platform.lower()
|
||||
if platform not in {"ios", "android", "web"}:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="platform must be one of: ios, android, web",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Upsert: reuse existing record if the token is already known.
|
||||
existing = (
|
||||
db.query(MobileDevice)
|
||||
.filter(MobileDevice.owner_id == owner_id, MobileDevice.push_token == body.push_token)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
existing.is_active = True
|
||||
existing.last_seen_at = now
|
||||
if body.device_name:
|
||||
existing.device_name = body.device_name
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
logger.info("Mobile device refreshed: id=%s owner=%s", existing.id, owner_id)
|
||||
return _device_to_response(existing)
|
||||
|
||||
device = MobileDevice(
|
||||
owner_id=owner_id,
|
||||
device_name=body.device_name,
|
||||
platform=platform,
|
||||
push_token=body.push_token,
|
||||
is_active=True,
|
||||
last_seen_at=now,
|
||||
)
|
||||
try:
|
||||
db.add(device)
|
||||
db.commit()
|
||||
db.refresh(device)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.exception("Failed to register mobile device for owner_id=%s", owner_id)
|
||||
raise
|
||||
|
||||
logger.info("Mobile device registered: id=%s owner=%s platform=%s", device.id, owner_id, platform)
|
||||
return _device_to_response(device)
|
||||
|
||||
|
||||
@router.get("/devices", response_model=list[DeviceResponse])
|
||||
@require_login
|
||||
async def list_devices(
|
||||
request: Request,
|
||||
owner_id: CurrentOwner,
|
||||
db: DbSession,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List all registered push-notification devices for the current user."""
|
||||
devices = (
|
||||
db.query(MobileDevice).filter(MobileDevice.owner_id == owner_id).order_by(MobileDevice.created_at.desc()).all()
|
||||
)
|
||||
return [_device_to_response(d) for d in devices]
|
||||
|
||||
|
||||
@router.delete("/devices/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@require_login
|
||||
async def deactivate_device(
|
||||
request: Request,
|
||||
device_id: int,
|
||||
owner_id: CurrentOwner,
|
||||
db: DbSession,
|
||||
) -> None:
|
||||
"""Deactivate a push-notification device registration.
|
||||
|
||||
The device record is kept for audit purposes but will no longer receive
|
||||
push notifications.
|
||||
"""
|
||||
device = db.get(MobileDevice, device_id)
|
||||
if not device or device.owner_id != owner_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found")
|
||||
|
||||
device.is_active = False
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
logger.info("Mobile device deactivated: id=%s owner=%s", device_id, owner_id)
|
||||
|
||||
|
||||
@router.get("/whoami", response_model=WhoAmIResponse)
|
||||
@require_login
|
||||
async def whoami(
|
||||
request: Request,
|
||||
owner_id: CurrentOwner,
|
||||
db: DbSession,
|
||||
) -> dict[str, Any]:
|
||||
"""Return basic profile information for the authenticated user.
|
||||
|
||||
The mobile app calls this after token exchange to populate the user
|
||||
profile screen and verify that the stored token is still valid.
|
||||
"""
|
||||
from app.auth import get_gravatar_url
|
||||
from app.models import LocalUser, UserProfile
|
||||
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
local_user = db.query(LocalUser).filter(LocalUser.email == owner_id).first()
|
||||
|
||||
display_name: str | None = None
|
||||
email: str | None = None
|
||||
avatar_url: str | None = None
|
||||
is_admin = False
|
||||
|
||||
if profile:
|
||||
display_name = profile.display_name
|
||||
|
||||
if local_user:
|
||||
email = local_user.email
|
||||
is_admin = bool(local_user.is_admin)
|
||||
if not display_name and local_user.display_name:
|
||||
display_name = local_user.display_name
|
||||
elif "@" in owner_id:
|
||||
# SSO users commonly have their email as owner_id
|
||||
email = owner_id
|
||||
|
||||
if email:
|
||||
avatar_url = get_gravatar_url(email)
|
||||
|
||||
return {
|
||||
"owner_id": owner_id,
|
||||
"display_name": display_name,
|
||||
"email": email,
|
||||
"avatar_url": avatar_url,
|
||||
"is_admin": is_admin,
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
"""User self-service profile API.
|
||||
|
||||
Provides endpoints for the authenticated user to view and update their own
|
||||
profile settings without requiring admin access.
|
||||
|
||||
Routes:
|
||||
GET /api/profile — read current user's profile
|
||||
PATCH /api/profile — update display name, language, theme
|
||||
POST /api/profile/avatar — upload a new profile picture (JPEG/PNG/GIF/WebP, max 2 MB)
|
||||
DELETE /api/profile/avatar — remove custom avatar (reverts to Gravatar)
|
||||
POST /api/profile/change-password — change password (local-auth users only)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from hashlib import md5
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import require_login
|
||||
from app.database import get_db
|
||||
from app.models import LocalUser, UserProfile
|
||||
from app.utils.i18n import SUPPORTED_LANGUAGE_CODES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/profile", tags=["profile"])
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
# Maximum avatar upload size: 2 MB
|
||||
_MAX_AVATAR_BYTES = 2 * 1024 * 1024
|
||||
|
||||
# Allowed MIME types for avatar uploads
|
||||
_ALLOWED_AVATAR_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||
|
||||
# Valid theme values
|
||||
_VALID_THEMES = {"light", "dark", "system"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_user_id(request: Request) -> str:
|
||||
"""Return the stable user identifier from the session.
|
||||
|
||||
Raises HTTP 401 if no user is logged in.
|
||||
"""
|
||||
user = request.session.get("user")
|
||||
if not user or not isinstance(user, dict):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
uid = user.get("sub") or user.get("preferred_username") or user.get("email") or user.get("id")
|
||||
if not uid:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Cannot determine user identity")
|
||||
return uid
|
||||
|
||||
|
||||
def _gravatar_url(email: str | None) -> str:
|
||||
"""Generate a Gravatar URL for *email*, falling back to identicon."""
|
||||
if not email:
|
||||
return "https://www.gravatar.com/avatar/?d=identicon"
|
||||
# MD5 used for Gravatar URL generation only — not for security
|
||||
h = md5(email.strip().lower().encode(), usedforsecurity=False).hexdigest()
|
||||
return f"https://www.gravatar.com/avatar/{h}?d=identicon"
|
||||
|
||||
|
||||
def _get_or_create_profile(db: Session, user_id: str) -> UserProfile:
|
||||
"""Return the UserProfile for *user_id*, creating a stub if one doesn't exist."""
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
if profile is None:
|
||||
profile = UserProfile(user_id=user_id)
|
||||
db.add(profile)
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(profile)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
return profile
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ProfileResponse(BaseModel):
|
||||
"""Response body for GET /api/profile."""
|
||||
|
||||
user_id: str
|
||||
display_name: str | None
|
||||
contact_email: str | None
|
||||
preferred_language: str | None
|
||||
preferred_theme: str | None
|
||||
avatar_url: str
|
||||
"""Gravatar URL or ``data:`` URI for a custom uploaded avatar."""
|
||||
is_local_user: bool
|
||||
"""True when the account was created via local email/password sign-up."""
|
||||
|
||||
|
||||
class ProfileUpdateRequest(BaseModel):
|
||||
"""Request body for PATCH /api/profile."""
|
||||
|
||||
display_name: str | None = Field(default=None, max_length=255, description="Human-readable display name")
|
||||
contact_email: str | None = Field(default=None, max_length=255, description="Contact / notification e-mail")
|
||||
preferred_language: str | None = Field(default=None, description="ISO 639-1 language code, e.g. 'en', 'de'")
|
||||
preferred_theme: str | None = Field(default=None, description="Colour scheme: 'light', 'dark', or 'system'")
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""Request body for POST /api/profile/change-password."""
|
||||
|
||||
current_password: str = Field(..., min_length=1, max_length=128)
|
||||
new_password: str = Field(..., min_length=8, max_length=128)
|
||||
new_password_confirm: str = Field(..., min_length=8, max_length=128)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("", response_model=ProfileResponse)
|
||||
@require_login
|
||||
async def get_profile(request: Request, db: DbSession) -> ProfileResponse:
|
||||
"""Return the current user's profile settings."""
|
||||
user_id = _get_user_id(request)
|
||||
profile = _get_or_create_profile(db, user_id)
|
||||
|
||||
session_user = request.session.get("user", {})
|
||||
email = session_user.get("email") if isinstance(session_user, dict) else None
|
||||
|
||||
# Determine avatar: prefer stored data, fall back to Gravatar
|
||||
avatar_url = profile.avatar_data if profile.avatar_data else _gravatar_url(email) # type: ignore[attr-defined]
|
||||
|
||||
# Check whether this is a local (email/password) account
|
||||
is_local = db.query(LocalUser).filter(LocalUser.username == user_id).first() is not None
|
||||
|
||||
return ProfileResponse(
|
||||
user_id=user_id,
|
||||
display_name=profile.display_name, # type: ignore[arg-type]
|
||||
contact_email=profile.contact_email, # type: ignore[arg-type]
|
||||
preferred_language=profile.preferred_language, # type: ignore[arg-type]
|
||||
preferred_theme=profile.preferred_theme, # type: ignore[arg-type]
|
||||
avatar_url=avatar_url,
|
||||
is_local_user=is_local,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("", response_model=ProfileResponse)
|
||||
@require_login
|
||||
async def update_profile(body: ProfileUpdateRequest, request: Request, db: DbSession) -> ProfileResponse:
|
||||
"""Update the current user's editable profile settings."""
|
||||
user_id = _get_user_id(request)
|
||||
profile = _get_or_create_profile(db, user_id)
|
||||
|
||||
# Validate language code
|
||||
if body.preferred_language is not None:
|
||||
lang = body.preferred_language.lower().strip()
|
||||
if lang and lang not in SUPPORTED_LANGUAGE_CODES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Unsupported language code: {lang}",
|
||||
)
|
||||
profile.preferred_language = lang or None # type: ignore[assignment]
|
||||
|
||||
# Validate theme
|
||||
if body.preferred_theme is not None:
|
||||
theme = body.preferred_theme.lower().strip()
|
||||
if theme and theme not in _VALID_THEMES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Invalid theme: {theme}. Must be one of: {', '.join(sorted(_VALID_THEMES))}",
|
||||
)
|
||||
profile.preferred_theme = theme or None # type: ignore[assignment]
|
||||
|
||||
if body.display_name is not None:
|
||||
profile.display_name = body.display_name.strip() or None # type: ignore[assignment]
|
||||
|
||||
if body.contact_email is not None:
|
||||
profile.contact_email = body.contact_email.strip() or None # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(profile)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
session_user = request.session.get("user", {})
|
||||
email = session_user.get("email") if isinstance(session_user, dict) else None
|
||||
avatar_url = profile.avatar_data if profile.avatar_data else _gravatar_url(email) # type: ignore[attr-defined]
|
||||
is_local = db.query(LocalUser).filter(LocalUser.username == user_id).first() is not None
|
||||
|
||||
return ProfileResponse(
|
||||
user_id=user_id,
|
||||
display_name=profile.display_name, # type: ignore[arg-type]
|
||||
contact_email=profile.contact_email, # type: ignore[arg-type]
|
||||
preferred_language=profile.preferred_language, # type: ignore[arg-type]
|
||||
preferred_theme=profile.preferred_theme, # type: ignore[arg-type]
|
||||
avatar_url=avatar_url,
|
||||
is_local_user=is_local,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/avatar", status_code=status.HTTP_200_OK)
|
||||
@require_login
|
||||
async def upload_avatar(
|
||||
request: Request,
|
||||
db: DbSession,
|
||||
file: UploadFile = File(..., description="Profile picture (JPEG, PNG, GIF or WebP; max 2 MB)"),
|
||||
) -> dict:
|
||||
"""Upload a new profile picture.
|
||||
|
||||
The image is stored as a base64-encoded data URL in ``UserProfile.avatar_data``.
|
||||
Accepts JPEG, PNG, GIF, or WebP files up to 2 MB.
|
||||
"""
|
||||
user_id = _get_user_id(request)
|
||||
|
||||
content_type = (file.content_type or "").lower()
|
||||
if content_type not in _ALLOWED_AVATAR_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
detail=f"Unsupported image type '{content_type}'. Allowed: JPEG, PNG, GIF, WebP.",
|
||||
)
|
||||
|
||||
# Check declared size first (available when the client sends a Content-Length header)
|
||||
if file.size is not None and file.size > _MAX_AVATAR_BYTES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail="Avatar image must be 2 MB or smaller.",
|
||||
)
|
||||
|
||||
# Read up to one byte past the limit so we can detect oversized uploads
|
||||
raw = await file.read(_MAX_AVATAR_BYTES + 1)
|
||||
if len(raw) > _MAX_AVATAR_BYTES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail="Avatar image must be 2 MB or smaller.",
|
||||
)
|
||||
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
data_url = f"data:{content_type};base64,{b64}"
|
||||
|
||||
profile = _get_or_create_profile(db, user_id)
|
||||
profile.avatar_data = data_url # type: ignore[assignment]
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
return {"avatar_url": data_url}
|
||||
|
||||
|
||||
@router.delete("/avatar", status_code=status.HTTP_200_OK)
|
||||
@require_login
|
||||
async def delete_avatar(request: Request, db: DbSession) -> dict:
|
||||
"""Remove the custom avatar and revert to the Gravatar fallback."""
|
||||
user_id = _get_user_id(request)
|
||||
profile = _get_or_create_profile(db, user_id)
|
||||
profile.avatar_data = None # type: ignore[assignment]
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
session_user = request.session.get("user", {})
|
||||
email = session_user.get("email") if isinstance(session_user, dict) else None
|
||||
return {"avatar_url": _gravatar_url(email)}
|
||||
|
||||
|
||||
@router.post("/change-password", status_code=status.HTTP_200_OK)
|
||||
@require_login
|
||||
async def change_password(body: ChangePasswordRequest, request: Request, db: DbSession) -> dict:
|
||||
"""Change the password for local (email/password) accounts.
|
||||
|
||||
Raises 403 if the account is not a local account or the current password is wrong.
|
||||
Raises 422 if the new passwords do not match.
|
||||
"""
|
||||
from app.utils.local_auth import hash_password, verify_password
|
||||
|
||||
user_id = _get_user_id(request)
|
||||
|
||||
local_user = db.query(LocalUser).filter(LocalUser.username == user_id).first()
|
||||
if local_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Password change is only available for local accounts.",
|
||||
)
|
||||
|
||||
if not verify_password(body.current_password, local_user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Current password is incorrect.",
|
||||
)
|
||||
|
||||
if body.new_password != body.new_password_confirm:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="New passwords do not match.",
|
||||
)
|
||||
|
||||
local_user.hashed_password = hash_password(body.new_password)
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
logger.info("Password changed for local user: %s", user_id)
|
||||
return {"detail": "Password changed successfully."}
|
||||
+20
-7
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import require_login
|
||||
from app.database import get_db
|
||||
from app.models import FileRecord
|
||||
from app.models import FileRecord, UserProfile
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,7 +22,7 @@ router = APIRouter()
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
async def whoami_handler(request: Request):
|
||||
async def whoami_handler(request: Request, db: Session):
|
||||
"""
|
||||
Returns user info if logged in, else 401.
|
||||
"""
|
||||
@@ -41,20 +41,33 @@ async def whoami_handler(request: Request):
|
||||
|
||||
# Add the gravatar URL to the user object instead of creating a new response
|
||||
user_response = user.copy() # Create a copy to avoid modifying the session
|
||||
user_response["picture"] = gravatar_url
|
||||
|
||||
# Check if the user has a custom avatar stored in their profile
|
||||
user_id = user.get("sub") or user.get("preferred_username") or user.get("email") or user.get("id")
|
||||
if user_id:
|
||||
try:
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
if profile and profile.avatar_data:
|
||||
user_response["picture"] = profile.avatar_data
|
||||
else:
|
||||
user_response["picture"] = gravatar_url
|
||||
except Exception:
|
||||
user_response["picture"] = gravatar_url
|
||||
else:
|
||||
user_response["picture"] = gravatar_url
|
||||
|
||||
return user_response
|
||||
|
||||
|
||||
# Register the same handler under two different paths
|
||||
@router.get("/whoami")
|
||||
async def whoami(request: Request):
|
||||
return await whoami_handler(request)
|
||||
async def whoami(request: Request, db: DbSession):
|
||||
return await whoami_handler(request, db)
|
||||
|
||||
|
||||
@router.get("/auth/whoami")
|
||||
async def auth_whoami(request: Request):
|
||||
return await whoami_handler(request)
|
||||
async def auth_whoami(request: Request, db: DbSession):
|
||||
return await whoami_handler(request, db)
|
||||
|
||||
|
||||
@router.get("/users/search")
|
||||
|
||||
+282
-1
@@ -15,12 +15,14 @@ from starlette.responses import RedirectResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.middleware.audit_log import get_client_ip
|
||||
|
||||
# Conditional imports: only used when multi_user_enabled=True. Imported here at
|
||||
# module level (not inside auth()) so they don't incur repeated import overhead.
|
||||
# Guards at call-sites ensure they are never *called* in single-user mode.
|
||||
from app.models import LocalUser as _LocalUser
|
||||
from app.models import UserProfile as _UserProfile
|
||||
from app.utils.i18n import translate as _translate
|
||||
from app.utils.local_auth import build_session_user as _build_session_user
|
||||
from app.utils.local_auth import verify_password as _verify_password
|
||||
|
||||
@@ -33,11 +35,15 @@ AUTH_ENABLED = settings.auth_enabled
|
||||
# Set up templates for authentication
|
||||
templates_dir = pathlib.Path(__file__).parents[1] / "frontend" / "templates"
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
templates.env.globals["_"] = lambda key, **kwargs: _translate(key, "en", **kwargs)
|
||||
|
||||
# Configure OAuth provider if credentials are provided
|
||||
OAUTH_CONFIGURED = False
|
||||
OAUTH_PROVIDER_NAME = "Single Sign-On"
|
||||
|
||||
# Social login providers that are enabled and registered
|
||||
SOCIAL_PROVIDERS: dict[str, dict[str, str]] = {}
|
||||
|
||||
if AUTH_ENABLED and settings.authentik_client_id and settings.authentik_client_secret:
|
||||
oauth.register(
|
||||
name="authentik",
|
||||
@@ -49,6 +55,68 @@ if AUTH_ENABLED and settings.authentik_client_id and settings.authentik_client_s
|
||||
OAUTH_CONFIGURED = True
|
||||
OAUTH_PROVIDER_NAME = settings.oauth_provider_name or "Authentik SSO"
|
||||
|
||||
# --- Social Login Providers ---------------------------------------------------
|
||||
if AUTH_ENABLED and settings.social_auth_google_enabled:
|
||||
if settings.social_auth_google_client_id and settings.social_auth_google_client_secret:
|
||||
oauth.register(
|
||||
name="google",
|
||||
client_id=settings.social_auth_google_client_id,
|
||||
client_secret=settings.social_auth_google_client_secret,
|
||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||
client_kwargs={"scope": "openid profile email"},
|
||||
)
|
||||
SOCIAL_PROVIDERS["google"] = {"name": "Google", "icon": "fab fa-google", "color": "red"}
|
||||
logger.info("Social login provider registered: Google")
|
||||
else:
|
||||
logger.warning("SOCIAL_AUTH_GOOGLE_ENABLED=true but client ID/secret not configured")
|
||||
|
||||
if AUTH_ENABLED and settings.social_auth_microsoft_enabled:
|
||||
if settings.social_auth_microsoft_client_id and settings.social_auth_microsoft_client_secret:
|
||||
tenant = settings.social_auth_microsoft_tenant or "common"
|
||||
oauth.register(
|
||||
name="microsoft",
|
||||
client_id=settings.social_auth_microsoft_client_id,
|
||||
client_secret=settings.social_auth_microsoft_client_secret,
|
||||
server_metadata_url=f"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
|
||||
client_kwargs={"scope": "openid profile email"},
|
||||
)
|
||||
SOCIAL_PROVIDERS["microsoft"] = {"name": "Microsoft", "icon": "fab fa-microsoft", "color": "blue"}
|
||||
logger.info("Social login provider registered: Microsoft (tenant=%s)", tenant)
|
||||
else:
|
||||
logger.warning("SOCIAL_AUTH_MICROSOFT_ENABLED=true but client ID/secret not configured")
|
||||
|
||||
if AUTH_ENABLED and settings.social_auth_apple_enabled:
|
||||
if settings.social_auth_apple_client_id and settings.social_auth_apple_team_id:
|
||||
oauth.register(
|
||||
name="apple",
|
||||
client_id=settings.social_auth_apple_client_id,
|
||||
server_metadata_url="https://appleid.apple.com/.well-known/openid-configuration",
|
||||
client_kwargs={
|
||||
"scope": "openid name email",
|
||||
"response_mode": "form_post",
|
||||
},
|
||||
)
|
||||
SOCIAL_PROVIDERS["apple"] = {"name": "Apple", "icon": "fab fa-apple", "color": "gray"}
|
||||
logger.info("Social login provider registered: Apple")
|
||||
else:
|
||||
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
|
||||
|
||||
if AUTH_ENABLED and settings.social_auth_dropbox_enabled:
|
||||
if settings.social_auth_dropbox_client_id and settings.social_auth_dropbox_client_secret:
|
||||
oauth.register(
|
||||
name="dropbox",
|
||||
client_id=settings.social_auth_dropbox_client_id,
|
||||
client_secret=settings.social_auth_dropbox_client_secret,
|
||||
authorize_url="https://www.dropbox.com/oauth2/authorize",
|
||||
access_token_url="https://api.dropboxapi.com/oauth2/token",
|
||||
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
|
||||
client_kwargs={"token_endpoint_auth_method": "client_secret_post"},
|
||||
)
|
||||
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
|
||||
logger.info("Social login provider registered: Dropbox")
|
||||
else:
|
||||
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -194,6 +262,7 @@ async def login(request: Request):
|
||||
"message": request.query_params.get("message"),
|
||||
"show_oauth": OAUTH_CONFIGURED,
|
||||
"oauth_provider_name": OAUTH_PROVIDER_NAME,
|
||||
"social_providers": SOCIAL_PROVIDERS,
|
||||
"app_version": settings.version,
|
||||
"csrf_token": getattr(request.state, "csrf_token", ""),
|
||||
# "Create account" link is only shown when multi-user mode AND local signup are both enabled
|
||||
@@ -211,6 +280,149 @@ async def oauth_login(request: Request):
|
||||
return await oauth.authentik.authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
async def social_login(request: Request, provider: str):
|
||||
"""Initiate a social login flow for the given provider.
|
||||
|
||||
Args:
|
||||
request: The current FastAPI request.
|
||||
provider: One of the registered social provider keys (google, microsoft, apple, dropbox).
|
||||
|
||||
Returns:
|
||||
A redirect to the provider's authorization page, or back to /login on error.
|
||||
"""
|
||||
if provider not in SOCIAL_PROVIDERS:
|
||||
return RedirectResponse(url="/login?error=Unknown+social+provider", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
redirect_uri = request.url_for("social_callback", provider=provider)
|
||||
oauth_client = getattr(oauth, provider, None)
|
||||
if oauth_client is None:
|
||||
return RedirectResponse(url="/login?error=Provider+not+configured", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
return await oauth_client.authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
def _normalize_social_userinfo(provider: str, token: dict, raw_userinfo: dict | None) -> dict:
|
||||
"""Normalize the userinfo payload from different social providers into a common format.
|
||||
|
||||
Returns a dict with keys: sub, email, name, preferred_username, picture.
|
||||
|
||||
Args:
|
||||
provider: The social provider key (google, microsoft, apple, dropbox).
|
||||
token: The OAuth token response from the provider. Included for future
|
||||
provider-specific claim extraction (e.g. ``id_token`` claims).
|
||||
raw_userinfo: The raw userinfo dict (may be None for providers without standard OIDC userinfo).
|
||||
|
||||
Returns:
|
||||
A normalized user-data dict compatible with the session user format.
|
||||
"""
|
||||
userinfo: dict = raw_userinfo or {}
|
||||
|
||||
if provider == "dropbox":
|
||||
# Dropbox returns a non-standard userinfo response
|
||||
email = userinfo.get("email", "")
|
||||
name_info = userinfo.get("name", {})
|
||||
display_name = name_info.get("display_name", "") if isinstance(name_info, dict) else str(name_info)
|
||||
return {
|
||||
"sub": userinfo.get("account_id", email),
|
||||
"email": email,
|
||||
"name": display_name,
|
||||
"preferred_username": email,
|
||||
"picture": userinfo.get("profile_photo_url", ""),
|
||||
}
|
||||
|
||||
# Standard OIDC providers (Google, Microsoft, Apple)
|
||||
return {
|
||||
"sub": userinfo.get("sub", ""),
|
||||
"email": userinfo.get("email", ""),
|
||||
"name": userinfo.get("name", ""),
|
||||
"preferred_username": userinfo.get("email", ""),
|
||||
"picture": userinfo.get("picture", ""),
|
||||
}
|
||||
|
||||
|
||||
async def social_callback(request: Request, provider: str, db: Session = Depends(get_db)):
|
||||
"""Handle the OAuth callback from a social login provider.
|
||||
|
||||
After the user authorizes with the social provider, this endpoint exchanges
|
||||
the authorization code for tokens, extracts user information, creates or
|
||||
updates the user profile, and establishes a session.
|
||||
|
||||
Args:
|
||||
request: The current FastAPI request.
|
||||
provider: One of the registered social provider keys.
|
||||
db: Database session (injected).
|
||||
|
||||
Returns:
|
||||
A redirect to the user's original destination or the upload page.
|
||||
"""
|
||||
if provider not in SOCIAL_PROVIDERS:
|
||||
return RedirectResponse(url="/login?error=Unknown+social+provider", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
oauth_client = getattr(oauth, provider, None)
|
||||
if oauth_client is None:
|
||||
return RedirectResponse(url="/login?error=Provider+not+configured", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
try:
|
||||
token = await oauth_client.authorize_access_token(request)
|
||||
|
||||
# Try standard OIDC userinfo first, fall back to token-embedded userinfo
|
||||
raw_userinfo = token.get("userinfo")
|
||||
if not raw_userinfo:
|
||||
try:
|
||||
resp = await oauth_client.userinfo(token=token)
|
||||
raw_userinfo = resp if isinstance(resp, dict) else resp.json() if hasattr(resp, "json") else {}
|
||||
except Exception:
|
||||
raw_userinfo = {}
|
||||
|
||||
user_data = _normalize_social_userinfo(provider, token, raw_userinfo)
|
||||
|
||||
if not user_data.get("email"):
|
||||
return RedirectResponse(
|
||||
url="/login?error=Could+not+retrieve+email+from+provider",
|
||||
status_code=status.HTTP_302_FOUND,
|
||||
)
|
||||
|
||||
# Add Gravatar if no picture provided
|
||||
if not user_data.get("picture") and user_data.get("email"):
|
||||
user_data["picture"] = get_gravatar_url(user_data["email"])
|
||||
|
||||
# Tag the login source for audit/debugging
|
||||
user_data["auth_provider"] = provider
|
||||
|
||||
# Social login users are never admin by default (admin must be granted
|
||||
# via the Authentik/OIDC admin group or manually in the admin panel)
|
||||
user_data["is_admin"] = False
|
||||
|
||||
request.session["user"] = user_data
|
||||
|
||||
# Auto-create or update UserProfile
|
||||
_ensure_user_profile(db, user_data, is_admin=False)
|
||||
|
||||
provider_name = SOCIAL_PROVIDERS[provider]["name"]
|
||||
logger.info(
|
||||
"[SECURITY] SOCIAL_LOGIN_SUCCESS provider=%s user=%s", provider_name, user_data.get("email", "unknown")
|
||||
)
|
||||
|
||||
# Redirect first-time users to onboarding
|
||||
user_id = (
|
||||
user_data.get("sub") or user_data.get("preferred_username") or user_data.get("email") or user_data.get("id")
|
||||
)
|
||||
if user_id:
|
||||
profile = db.query(_UserProfile).filter(_UserProfile.user_id == user_id).first()
|
||||
if profile and not profile.onboarding_completed:
|
||||
post_onboarding = request.session.pop("redirect_after_login", "/upload")
|
||||
request.session["post_onboarding_redirect"] = post_onboarding
|
||||
return RedirectResponse(url="/onboarding", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url, status_code=status.HTTP_302_FOUND)
|
||||
except Exception as e:
|
||||
logger.warning("[SECURITY] SOCIAL_LOGIN_FAILURE provider=%s error=%s", provider, type(e).__name__)
|
||||
return RedirectResponse(
|
||||
url="/login?error=Social+login+failed.+Please+try+again.", status_code=status.HTTP_302_FOUND
|
||||
)
|
||||
|
||||
|
||||
def _ensure_user_profile(db: Session, user_data: dict, is_admin: bool = False) -> None:
|
||||
"""Create or update a UserProfile row for *user_data*.
|
||||
|
||||
@@ -344,6 +556,13 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
|
||||
|
||||
# Log the successful authentication
|
||||
logger.info("[SECURITY] OAUTH_LOGIN_SUCCESS user=%s admin=%s", user_data.get("email", "unknown"), is_admin)
|
||||
_record_login_event(
|
||||
db,
|
||||
request,
|
||||
user_data.get("email") or user_data.get("preferred_username") or "unknown",
|
||||
success=True,
|
||||
method="oauth",
|
||||
)
|
||||
|
||||
# Redirect first-time users to onboarding
|
||||
user_id = (
|
||||
@@ -364,6 +583,48 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
|
||||
return RedirectResponse(url=f"/login?error=Authentication+failed:+{str(e)}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
def _record_login_event(
|
||||
db: Session,
|
||||
request: Request,
|
||||
username: str,
|
||||
*,
|
||||
success: bool,
|
||||
method: str = "local",
|
||||
detail: str | None = None,
|
||||
) -> None:
|
||||
"""Write a login or login-failure audit event to the database.
|
||||
|
||||
Failures are silently swallowed so that an audit-service error never
|
||||
prevents a legitimate login or surfaces an unrelated 500 error to the user.
|
||||
|
||||
Args:
|
||||
db: Active database session.
|
||||
request: The current HTTP request (used to extract the client IP).
|
||||
username: The username that attempted authentication.
|
||||
success: ``True`` for a successful login, ``False`` for a failure.
|
||||
method: Authentication method, e.g. ``"local"`` or ``"oauth"``.
|
||||
detail: Optional extra context for failures (e.g. ``"wrong_password"``).
|
||||
"""
|
||||
try:
|
||||
from app.utils.audit_service import record_event
|
||||
|
||||
action = "login" if success else "login.failure"
|
||||
details: dict = {"method": method}
|
||||
if detail:
|
||||
details["reason"] = detail
|
||||
record_event(
|
||||
db,
|
||||
action=action,
|
||||
user=username,
|
||||
resource_type="session",
|
||||
ip_address=get_client_ip(request),
|
||||
details=details,
|
||||
severity="info" if success else "warning",
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to write login audit event for user=%s", username, exc_info=True)
|
||||
|
||||
|
||||
async def auth(request: Request, db: Session = Depends(get_db)):
|
||||
"""Handle local username/password authentication.
|
||||
|
||||
@@ -413,6 +674,7 @@ async def auth(request: Request, db: Session = Depends(get_db)):
|
||||
username,
|
||||
local_user.is_active,
|
||||
)
|
||||
_record_login_event(db, request, username, success=False, detail="account_not_verified")
|
||||
return RedirectResponse(
|
||||
url="/login?error=Please+verify+your+email+address+before+logging+in",
|
||||
status_code=302,
|
||||
@@ -425,10 +687,12 @@ async def auth(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
if not pw_ok:
|
||||
logger.warning("[SECURITY] LOCAL_LOGIN_FAILURE reason=wrong_password user=%s", username)
|
||||
_record_login_event(db, request, username, success=False, detail="wrong_password")
|
||||
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
|
||||
user_data = _build_session_user(local_user)
|
||||
request.session["user"] = user_data
|
||||
logger.info("[SECURITY] LOCAL_LOGIN_SUCCESS user=%s", local_user.email)
|
||||
_record_login_event(db, request, local_user.email, success=True)
|
||||
_ensure_user_profile(db, user_data, is_admin=bool(local_user.is_admin))
|
||||
profile = db.query(_UserProfile).filter(_UserProfile.user_id == local_user.email).first()
|
||||
if profile and not profile.onboarding_completed:
|
||||
@@ -473,6 +737,7 @@ async def auth(request: Request, db: Session = Depends(get_db)):
|
||||
}
|
||||
request.session["user"] = admin_user_data
|
||||
logger.info("[SECURITY] LOCAL_LOGIN_SUCCESS user=%s", username)
|
||||
_record_login_event(db, request, username, success=True)
|
||||
_ensure_user_profile(db, admin_user_data, is_admin=True)
|
||||
redirect_url = request.session.pop("redirect_after_login", "/upload")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
@@ -485,16 +750,30 @@ async def auth(request: Request, db: Session = Depends(get_db)):
|
||||
admin_configured,
|
||||
not username and not password,
|
||||
)
|
||||
_record_login_event(db, request, username or "anonymous", success=False, detail="invalid_credentials")
|
||||
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
|
||||
|
||||
|
||||
async def logout(request: Request):
|
||||
async def logout(request: Request, db: Session = Depends(get_db)):
|
||||
"""Handle user logout"""
|
||||
user = request.session.get("user")
|
||||
username = "unknown"
|
||||
if isinstance(user, dict):
|
||||
username = user.get("preferred_username") or user.get("email") or "unknown"
|
||||
logger.info(f"[SECURITY] LOGOUT user={username}")
|
||||
try:
|
||||
from app.utils.audit_service import record_event
|
||||
|
||||
record_event(
|
||||
db,
|
||||
action="logout",
|
||||
user=username,
|
||||
resource_type="session",
|
||||
ip_address=get_client_ip(request),
|
||||
severity="info",
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to write logout audit event for user=%s", username, exc_info=True)
|
||||
request.session.pop("user", None)
|
||||
return RedirectResponse(url="/login?message=You+have+been+logged+out+successfully", status_code=302)
|
||||
|
||||
@@ -503,6 +782,8 @@ if AUTH_ENABLED:
|
||||
router.add_api_route("/login", login, methods=["GET"])
|
||||
router.add_api_route("/oauth-login", oauth_login, methods=["GET"])
|
||||
router.add_api_route("/oauth-callback", oauth_callback, methods=["GET"])
|
||||
router.add_api_route("/social-login/{provider}", social_login, methods=["GET"])
|
||||
router.add_api_route("/social-callback/{provider}", social_callback, methods=["GET"])
|
||||
router.add_api_route("/auth", auth, methods=["POST"])
|
||||
router.add_api_route("/logout", logout, methods=["GET"])
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ from app.tasks.upload_to_dropbox import upload_to_dropbox # noqa: F401
|
||||
from app.tasks.upload_to_email import upload_to_email # noqa: F401
|
||||
from app.tasks.upload_to_ftp import upload_to_ftp # noqa: F401
|
||||
from app.tasks.upload_to_google_drive import upload_to_google_drive # noqa: F401
|
||||
from app.tasks.upload_to_icloud import upload_to_icloud # noqa: F401
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud # noqa: F401
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive # noqa: F401
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless # noqa: F401
|
||||
|
||||
+145
-1
@@ -49,18 +49,30 @@ class Settings(BaseSettings):
|
||||
debug: bool = False # Default to False
|
||||
|
||||
# Making Dropbox optional
|
||||
dropbox_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Dropbox as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
dropbox_app_key: Optional[str] = None
|
||||
dropbox_app_secret: Optional[str] = None
|
||||
dropbox_folder: Optional[str] = None
|
||||
dropbox_refresh_token: Optional[str] = None
|
||||
|
||||
# Making Nextcloud optional
|
||||
nextcloud_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Nextcloud as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
nextcloud_upload_url: Optional[str] = None
|
||||
nextcloud_username: Optional[str] = None
|
||||
nextcloud_password: Optional[str] = None
|
||||
nextcloud_folder: Optional[str] = None
|
||||
|
||||
# Making Paperless optional
|
||||
paperless_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Paperless-ngx as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
paperless_ngx_api_token: Optional[str] = None
|
||||
paperless_host: Optional[str] = None
|
||||
paperless_custom_field_absender: Optional[str] = None # Name of the "absender" custom field in Paperless
|
||||
@@ -166,12 +178,44 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
# Authentik
|
||||
# Authentik / Generic OIDC
|
||||
authentik_client_id: Optional[str] = None
|
||||
authentik_client_secret: Optional[str] = None
|
||||
authentik_config_url: Optional[str] = None
|
||||
oauth_provider_name: Optional[str] = None # Name to display for the OAuth provider
|
||||
|
||||
# Social Login Providers
|
||||
# Google OAuth2
|
||||
social_auth_google_enabled: bool = False
|
||||
social_auth_google_client_id: Optional[str] = None
|
||||
social_auth_google_client_secret: Optional[str] = None
|
||||
|
||||
# Microsoft OAuth2 (Azure AD / Microsoft Entra ID)
|
||||
social_auth_microsoft_enabled: bool = False
|
||||
social_auth_microsoft_client_id: Optional[str] = None
|
||||
social_auth_microsoft_client_secret: Optional[str] = None
|
||||
social_auth_microsoft_tenant: str = Field(
|
||||
default="common",
|
||||
description=(
|
||||
"Azure AD tenant ID or one of 'common', 'organizations', 'consumers'. "
|
||||
"Use 'common' to allow any Microsoft account and any Azure AD org. "
|
||||
"Use a specific tenant ID (GUID) to restrict to a single organization. "
|
||||
"Default: common."
|
||||
),
|
||||
)
|
||||
|
||||
# Apple Sign-In
|
||||
social_auth_apple_enabled: bool = False
|
||||
social_auth_apple_client_id: Optional[str] = None
|
||||
social_auth_apple_team_id: Optional[str] = None
|
||||
social_auth_apple_key_id: Optional[str] = None
|
||||
social_auth_apple_private_key: Optional[str] = None
|
||||
|
||||
# Dropbox OAuth2
|
||||
social_auth_dropbox_enabled: bool = False
|
||||
social_auth_dropbox_client_id: Optional[str] = None
|
||||
social_auth_dropbox_client_secret: Optional[str] = None
|
||||
|
||||
# Local user signup
|
||||
allow_local_signup: bool = Field(
|
||||
default=False,
|
||||
@@ -394,6 +438,10 @@ class Settings(BaseSettings):
|
||||
imap2_delete_after_process: bool = False
|
||||
|
||||
# Google Drive settings
|
||||
google_drive_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Google Drive as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
google_drive_credentials_json: Optional[str] = ""
|
||||
google_drive_folder_id: Optional[str] = ""
|
||||
google_drive_delegate_to: Optional[str] = "" # Optional delegated user email
|
||||
@@ -405,6 +453,10 @@ class Settings(BaseSettings):
|
||||
google_drive_refresh_token: Optional[str] = ""
|
||||
|
||||
# WebDAV settings
|
||||
webdav_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable WebDAV as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
webdav_url: Optional[str] = None
|
||||
webdav_username: Optional[str] = None
|
||||
webdav_password: Optional[str] = None
|
||||
@@ -412,6 +464,10 @@ class Settings(BaseSettings):
|
||||
webdav_verify_ssl: bool = True
|
||||
|
||||
# FTP settings
|
||||
ftp_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable FTP as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
ftp_host: Optional[str] = None
|
||||
ftp_port: Optional[int] = 21
|
||||
ftp_username: Optional[str] = None
|
||||
@@ -421,6 +477,10 @@ class Settings(BaseSettings):
|
||||
ftp_allow_plaintext: bool = True # Default to allowing plaintext fallback
|
||||
|
||||
# SFTP settings
|
||||
sftp_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable SFTP as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
sftp_host: Optional[str] = None
|
||||
sftp_port: Optional[int] = 22
|
||||
sftp_username: Optional[str] = None
|
||||
@@ -442,6 +502,10 @@ class Settings(BaseSettings):
|
||||
email_default_recipient: Optional[str] = None
|
||||
|
||||
# Email destination settings (dedicated SMTP for document delivery – decoupled from shared email above)
|
||||
dest_email_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Email as an upload destination. Set to False to disable document delivery via email even when credentials are configured.",
|
||||
)
|
||||
dest_email_host: Optional[str] = None
|
||||
dest_email_port: Optional[int] = 587
|
||||
dest_email_username: Optional[str] = None
|
||||
@@ -451,6 +515,10 @@ class Settings(BaseSettings):
|
||||
dest_email_default_recipient: Optional[str] = None # Fallback recipient for document delivery
|
||||
|
||||
# OneDrive settings
|
||||
onedrive_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable OneDrive as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
onedrive_client_id: Optional[str] = None
|
||||
onedrive_client_secret: Optional[str] = None
|
||||
onedrive_tenant_id: Optional[str] = "common" # Default to "common" for personal accounts
|
||||
@@ -458,6 +526,10 @@ class Settings(BaseSettings):
|
||||
onedrive_folder_path: Optional[str] = None
|
||||
|
||||
# AWS S3 settings
|
||||
s3_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable Amazon S3 as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
aws_access_key_id: Optional[str] = None
|
||||
aws_secret_access_key: Optional[str] = None
|
||||
aws_region: Optional[str] = "us-east-1" # Default region
|
||||
@@ -466,6 +538,16 @@ class Settings(BaseSettings):
|
||||
s3_storage_class: Optional[str] = "STANDARD" # Default storage class
|
||||
s3_acl: Optional[str] = "private" # Default ACL
|
||||
|
||||
# iCloud Drive settings
|
||||
icloud_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable iCloud Drive as an upload destination. Set to False to disable uploads even when credentials are configured.",
|
||||
)
|
||||
icloud_username: Optional[str] = None # Apple ID email address
|
||||
icloud_password: Optional[str] = None # App-specific password (required for 2FA accounts)
|
||||
icloud_folder: Optional[str] = None # Target folder path in iCloud Drive (e.g. "Documents/Uploads")
|
||||
icloud_cookie_directory: Optional[str] = None # Directory for session cookies (default: ~/.pyicloud)
|
||||
|
||||
# Uptime Kuma settings
|
||||
uptime_kuma_url: Optional[str] = None
|
||||
uptime_kuma_ping_interval: int = 5 # Default ping interval in minutes
|
||||
@@ -484,6 +566,14 @@ class Settings(BaseSettings):
|
||||
|
||||
# Feature flags
|
||||
allow_file_delete: bool = True # Default to allowing file deletion from database
|
||||
compliance_enabled: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Enable the compliance templates dashboard (GDPR, HIPAA, SOC 2). "
|
||||
"When enabled, admins can view compliance status and apply "
|
||||
"pre-built regulatory configurations. Default: True."
|
||||
),
|
||||
)
|
||||
|
||||
# PDF/A archival conversion settings
|
||||
enable_pdfa_conversion: bool = Field(
|
||||
@@ -557,6 +647,17 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
imap_attachment_filter: str = Field(
|
||||
default="documents_only",
|
||||
description=(
|
||||
"Controls which attachment types are ingested from IMAP emails. "
|
||||
"Accepted values: "
|
||||
"'documents_only' – ingest only PDFs and office files (Word, Excel, PowerPoint, ODT, etc.); "
|
||||
"'all' – ingest all supported file types including images. "
|
||||
"This is the global default; individual user IMAP accounts can override it."
|
||||
),
|
||||
)
|
||||
|
||||
# Batch processing settings
|
||||
processall_throttle_threshold: int = Field(
|
||||
default=20,
|
||||
@@ -849,6 +950,49 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
# SIEM / External Audit Log Forwarding
|
||||
# Forward audit events to external SIEM systems for centralised monitoring.
|
||||
audit_siem_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Enable forwarding of audit events to an external SIEM system.",
|
||||
)
|
||||
audit_siem_transport: str = Field(
|
||||
default="syslog",
|
||||
description=(
|
||||
"Transport used to forward audit events. "
|
||||
"Options: 'syslog' (RFC 5424 over UDP/TCP), 'http' (JSON POST to a webhook URL, "
|
||||
"compatible with Splunk HEC, Logstash HTTP input, Grafana Loki, etc.)."
|
||||
),
|
||||
)
|
||||
audit_siem_syslog_host: str = Field(
|
||||
default="localhost",
|
||||
description="Hostname or IP of the syslog receiver.",
|
||||
)
|
||||
audit_siem_syslog_port: int = Field(
|
||||
default=514,
|
||||
description="Port of the syslog receiver.",
|
||||
)
|
||||
audit_siem_syslog_protocol: str = Field(
|
||||
default="udp",
|
||||
description="Protocol for syslog transport: 'udp' or 'tcp'.",
|
||||
)
|
||||
audit_siem_http_url: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"HTTP endpoint URL for SIEM webhook delivery. "
|
||||
"Supports Splunk HEC (https://splunk:8088/services/collector/event), "
|
||||
"Logstash HTTP input, Grafana Loki push API, or any JSON-accepting endpoint."
|
||||
),
|
||||
)
|
||||
audit_siem_http_token: str = Field(
|
||||
default="",
|
||||
description="Bearer / HEC token included in the Authorization header of SIEM HTTP requests.",
|
||||
)
|
||||
audit_siem_http_custom_headers: str = Field(
|
||||
default="",
|
||||
description="Comma-separated 'Key:Value' pairs of extra headers for SIEM HTTP requests.",
|
||||
)
|
||||
|
||||
# UI / Appearance
|
||||
ui_default_color_scheme: str = Field(
|
||||
default="system",
|
||||
|
||||
+36
-6
@@ -16,6 +16,7 @@ from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
|
||||
from app.api import router as api_router
|
||||
from app.api.graphql_api import graphql_router
|
||||
from app.api.local_auth import router as local_auth_router
|
||||
from app.auth import router as auth_router
|
||||
from app.config import settings
|
||||
@@ -146,6 +147,20 @@ async def lifespan(app: FastAPI):
|
||||
except Exception:
|
||||
logging.debug("Scheduled jobs seeding skipped — DB may not be ready yet") # noqa: S110
|
||||
|
||||
# Seed the built-in compliance templates (GDPR, HIPAA, SOC2) so they
|
||||
# are available in the admin compliance dashboard on first startup.
|
||||
try:
|
||||
from app.database import SessionLocal as _SessionLocal # noqa: F811
|
||||
from app.utils.compliance_service import seed_compliance_templates as _seed_compliance
|
||||
|
||||
_db_compliance = _SessionLocal()
|
||||
try:
|
||||
_seed_compliance(_db_compliance)
|
||||
finally:
|
||||
_db_compliance.close()
|
||||
except Exception:
|
||||
logging.debug("Compliance template seeding skipped — DB may not be ready yet") # noqa: S110
|
||||
|
||||
# Application is now running
|
||||
yield
|
||||
|
||||
@@ -239,6 +254,21 @@ else:
|
||||
|
||||
|
||||
# Custom exception handlers that return JSON for API routes and HTML for frontend routes
|
||||
# These use their own separate templates instance so that patches in tests on individual
|
||||
# view modules do not affect the error handler rendering.
|
||||
_error_templates_dir = pathlib.Path(__file__).parents[1] / "frontend" / "templates"
|
||||
_error_templates = Jinja2Templates(directory=str(_error_templates_dir))
|
||||
# Register the i18n translate helper as a global so error templates can use {{ _("key") }}.
|
||||
# Error pages use the default language (English); request-specific locale is not needed here.
|
||||
from app.utils.i18n import SUPPORTED_LANGUAGES as _SUPPORTED_LANGUAGES # noqa: E402
|
||||
from app.utils.i18n import translate as _translate_fn # noqa: E402
|
||||
|
||||
_error_templates.env.globals["_"] = lambda key, **kwargs: _translate_fn(key, "en", **kwargs)
|
||||
_error_templates.env.globals["min"] = min
|
||||
_error_templates.env.globals["max"] = max
|
||||
_error_templates.env.globals["supported_languages"] = _SUPPORTED_LANGUAGES
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
"""
|
||||
@@ -250,15 +280,15 @@ async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
|
||||
|
||||
# For frontend routes, return appropriate HTML templates
|
||||
templates = Jinja2Templates(directory=str(static_dir.parent / "templates"))
|
||||
|
||||
# Handle 404 errors with a custom template
|
||||
if exc.status_code == 404:
|
||||
return templates.TemplateResponse("404.html", {"request": request}, status_code=status.HTTP_404_NOT_FOUND)
|
||||
return _error_templates.TemplateResponse(
|
||||
"404.html", {"request": request}, status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# For other HTTP errors, we could create specific templates or use a generic one
|
||||
# For now, return a simple error page
|
||||
return templates.TemplateResponse(
|
||||
return _error_templates.TemplateResponse(
|
||||
"404.html", # Reuse 404 template for other errors, or create a generic error template
|
||||
{"request": request},
|
||||
status_code=exc.status_code,
|
||||
@@ -279,8 +309,7 @@ async def custom_500_handler(request: Request, exc: Exception):
|
||||
)
|
||||
|
||||
# Serve the 500 template for non-API routes
|
||||
templates = Jinja2Templates(directory=str(static_dir.parent / "templates"))
|
||||
return templates.TemplateResponse(
|
||||
return _error_templates.TemplateResponse(
|
||||
"500.html",
|
||||
{"request": request, "exc": exc},
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -298,3 +327,4 @@ app.include_router(files_router) # Explicitly include the files router
|
||||
app.include_router(auth_router)
|
||||
app.include_router(local_auth_router)
|
||||
app.include_router(api_router, prefix="/api")
|
||||
app.include_router(graphql_router, prefix="/graphql")
|
||||
|
||||
+137
@@ -147,6 +147,27 @@ class SettingsAuditLog(Base):
|
||||
action = Column(String, nullable=False) # "update" or "delete"
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Comprehensive audit log for compliance tracking.
|
||||
|
||||
Records all significant actions: login/logout, document CRUD, settings
|
||||
changes, and administrative operations. Rows are append-only; the API
|
||||
and service layer never update or delete entries.
|
||||
"""
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
user = Column(String, nullable=False, index=True) # Username or "anonymous" / "system"
|
||||
action = Column(String, nullable=False, index=True) # e.g. "login", "document.create", "settings.update"
|
||||
resource_type = Column(String, nullable=True, index=True) # e.g. "document", "user", "settings"
|
||||
resource_id = Column(String, nullable=True) # ID of the affected resource
|
||||
ip_address = Column(String, nullable=True) # Client IP address
|
||||
details = Column(Text, nullable=True) # JSON-encoded extra context
|
||||
severity = Column(String(16), nullable=False, server_default="info") # info / warning / error / critical
|
||||
|
||||
|
||||
class SavedSearch(Base):
|
||||
"""User-defined saved search filters for quick access to frequently used filter combinations."""
|
||||
|
||||
@@ -257,6 +278,17 @@ class UserProfile(Base):
|
||||
preferred_destination = Column(String(50), nullable=True)
|
||||
stripe_customer_id = Column(String(64), nullable=True)
|
||||
|
||||
# UI language preference for i18n (ISO 639-1 code, e.g. "en", "de", "fr")
|
||||
# NULL means "auto-detect from browser Accept-Language header"
|
||||
preferred_language = Column(String(10), nullable=True)
|
||||
|
||||
# UI colour scheme preference: "light" | "dark" | "system" (NULL = "system")
|
||||
preferred_theme = Column(String(10), nullable=True)
|
||||
|
||||
# Custom profile avatar stored as a base64 data-URL (e.g. "data:image/png;base64,...")
|
||||
# NULL means use the Gravatar fallback derived from the user's e-mail address.
|
||||
avatar_data = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
@@ -377,6 +409,48 @@ class PipelineStep(Base):
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class ImapIngestionProfile(Base):
|
||||
"""Named ingestion profile controlling which attachment types are accepted from IMAP emails.
|
||||
|
||||
Profiles group file-type categories (e.g. "pdf", "office", "images") so users
|
||||
can precisely control what gets ingested from each mailbox.
|
||||
|
||||
System-provided built-in profiles (``is_builtin=True``) are seeded by the
|
||||
migration and cannot be deleted or renamed. Users may create their own profiles
|
||||
(``owner_id`` set to their identifier) or rely on the global system profiles
|
||||
(``owner_id=None``).
|
||||
|
||||
``allowed_categories`` stores a JSON list of category strings, e.g.::
|
||||
|
||||
'["pdf", "office", "opendocument", "text", "web"]'
|
||||
|
||||
Valid category names are defined in ``app.utils.allowed_types.FILE_TYPE_CATEGORIES``.
|
||||
"""
|
||||
|
||||
__tablename__ = "imap_ingestion_profiles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Human-readable profile name (e.g. "Documents Only", "Documents + Images")
|
||||
name = Column(String(255), nullable=False)
|
||||
|
||||
# Optional description shown in the UI
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Owner of this profile. NULL = global/system profile available to all users.
|
||||
owner_id = Column(String, nullable=True, index=True)
|
||||
|
||||
# JSON-encoded list of enabled category keys. Example: '["pdf","office","text"]'
|
||||
# See FILE_TYPE_CATEGORIES in app/utils/allowed_types.py for valid values.
|
||||
allowed_categories = Column(Text, nullable=False, default='["pdf","office","opendocument","text","web"]')
|
||||
|
||||
# Built-in system profiles that cannot be deleted or modified via the API.
|
||||
is_builtin = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class UserImapAccount(Base):
|
||||
"""Per-user IMAP ingestion account.
|
||||
|
||||
@@ -415,6 +489,10 @@ class UserImapAccount(Base):
|
||||
# When True, emails are deleted from the mailbox after their attachments are processed
|
||||
delete_after_process = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
# Optional reference to an ImapIngestionProfile.
|
||||
# NULL means "use the global imap_attachment_filter setting" (system default).
|
||||
profile_id = Column(Integer, ForeignKey("imap_ingestion_profiles.id"), nullable=True)
|
||||
|
||||
# When False the account is not polled by the periodic task (but not deleted)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
@@ -506,6 +584,7 @@ class IntegrationType:
|
||||
EMAIL = "EMAIL"
|
||||
PAPERLESS = "PAPERLESS"
|
||||
RCLONE = "RCLONE"
|
||||
ICLOUD = "ICLOUD"
|
||||
|
||||
ALL = {
|
||||
IMAP,
|
||||
@@ -522,6 +601,7 @@ class IntegrationType:
|
||||
EMAIL,
|
||||
PAPERLESS,
|
||||
RCLONE,
|
||||
ICLOUD,
|
||||
}
|
||||
|
||||
|
||||
@@ -836,6 +916,63 @@ class ScheduledJob(Base):
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class MobileDevice(Base):
|
||||
"""Registered mobile device for push notifications.
|
||||
|
||||
Stores the push token (Expo push token, FCM token, or APNs token) for a
|
||||
specific user device so that document-processing events can be forwarded
|
||||
as push notifications to the native mobile app.
|
||||
"""
|
||||
|
||||
__tablename__ = "mobile_devices"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# User that owns this device registration.
|
||||
owner_id = Column(String, nullable=False, index=True)
|
||||
|
||||
# Human-readable name the user gave this device (e.g. "John's iPhone").
|
||||
device_name = Column(String(255), nullable=True)
|
||||
|
||||
# Platform: "ios", "android", or "web".
|
||||
platform = Column(String(20), nullable=False, default="ios")
|
||||
|
||||
# Expo push token (ExponentPushToken[…]) or raw FCM/APNs token.
|
||||
push_token = Column(String(512), nullable=False)
|
||||
|
||||
# Whether push notifications are enabled for this device.
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
# Timestamps.
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_seen_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
__table_args__ = (UniqueConstraint("owner_id", "push_token", name="uq_mobile_device_owner_token"),)
|
||||
|
||||
|
||||
class ComplianceTemplate(Base):
|
||||
"""Pre-built compliance configuration templates (GDPR, HIPAA, SOC2).
|
||||
|
||||
Each row represents an applied compliance template. The ``settings_json``
|
||||
column stores the concrete setting key/value pairs that were written when
|
||||
the template was applied. ``status`` tracks the current compliance posture.
|
||||
"""
|
||||
|
||||
__tablename__ = "compliance_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), unique=True, nullable=False, index=True) # GDPR, HIPAA, SOC2
|
||||
display_name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
settings_json = Column(Text, nullable=False, default="{}") # JSON of applied settings
|
||||
enabled = Column(Boolean, nullable=False, default=False)
|
||||
status = Column(String(20), nullable=False, default="not_applied") # not_applied, compliant, partial, non_compliant
|
||||
applied_at = Column(DateTime(timezone=True), nullable=True)
|
||||
applied_by = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class PipelineRoutingRule(Base):
|
||||
"""Conditional routing rule that assigns documents to pipelines.
|
||||
|
||||
|
||||
+92
-25
@@ -13,7 +13,11 @@ from celery import shared_task
|
||||
from app.config import settings
|
||||
from app.tasks.convert_to_pdf import convert_to_pdf # new conversion task
|
||||
from app.tasks.process_document import process_document # Updated import
|
||||
from app.utils.allowed_types import ALLOWED_EXTENSIONS, ALLOWED_MIME_TYPES
|
||||
from app.utils.allowed_types import (
|
||||
ALL_CATEGORIES,
|
||||
DEFAULT_CATEGORIES,
|
||||
get_allowed_types_for_categories,
|
||||
)
|
||||
|
||||
# Database session for per-user IMAP accounts (imported lazily to avoid circular imports)
|
||||
_db_session_factory = None
|
||||
@@ -50,6 +54,38 @@ def _decrypt_imap_password(password: str | None) -> str | None:
|
||||
return decrypt_value(password)
|
||||
|
||||
|
||||
def _resolve_categories_for_profile(profile_id: int | None) -> list[str]:
|
||||
"""Return the list of allowed categories for a profile ID.
|
||||
|
||||
Loads the profile from the database. If ``profile_id`` is ``None`` or the
|
||||
profile is not found, falls back to the global ``settings.imap_attachment_filter``
|
||||
string (``'documents_only'`` → default categories; ``'all'`` → all categories).
|
||||
"""
|
||||
if profile_id is not None:
|
||||
try:
|
||||
from app.models import ImapIngestionProfile
|
||||
|
||||
db = _get_db_session()
|
||||
try:
|
||||
profile = db.query(ImapIngestionProfile).filter(ImapIngestionProfile.id == profile_id).first()
|
||||
if profile:
|
||||
return json.loads(profile.allowed_categories)
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning(
|
||||
"Could not load IMAP ingestion profile %d (%s: %s) — using global default",
|
||||
profile_id,
|
||||
type(exc).__name__,
|
||||
exc,
|
||||
)
|
||||
|
||||
# Fall back to global setting
|
||||
if settings.imap_attachment_filter == "all":
|
||||
return ALL_CATEGORIES
|
||||
return DEFAULT_CATEGORIES
|
||||
|
||||
|
||||
LOCK_KEY = "imap_lock" # Unique key for locking
|
||||
LOCK_EXPIRE = 300 # Lock expires in 5 minutes
|
||||
|
||||
@@ -180,6 +216,7 @@ def _pull_user_imap_accounts() -> None:
|
||||
use_ssl=acct.use_ssl,
|
||||
delete_after_process=acct.delete_after_process,
|
||||
owner_id=acct.owner_id,
|
||||
allowed_categories=_resolve_categories_for_profile(acct.profile_id),
|
||||
)
|
||||
# Record successful poll
|
||||
acct.last_checked_at = datetime.now(timezone.utc)
|
||||
@@ -250,6 +287,9 @@ def _pull_user_integration_imap() -> None:
|
||||
use_ssl = cfg.get("use_ssl", True)
|
||||
delete_after = cfg.get("delete_after_process", False)
|
||||
gmail_labels = cfg.get("gmail_apply_labels", True)
|
||||
# Integrations can store a profile_id in config; fall back to global default
|
||||
profile_id = cfg.get("profile_id")
|
||||
allowed_categories = _resolve_categories_for_profile(profile_id)
|
||||
|
||||
if not (host and username and password):
|
||||
logger.warning(
|
||||
@@ -269,6 +309,7 @@ def _pull_user_integration_imap() -> None:
|
||||
delete_after_process=delete_after,
|
||||
owner_id=integ.owner_id,
|
||||
gmail_apply_labels=gmail_labels,
|
||||
allowed_categories=allowed_categories,
|
||||
)
|
||||
integ.last_used_at = datetime.now(timezone.utc)
|
||||
integ.last_error = None
|
||||
@@ -329,6 +370,7 @@ def pull_inbox(
|
||||
delete_after_process,
|
||||
owner_id=None,
|
||||
gmail_apply_labels=True,
|
||||
allowed_categories=None,
|
||||
):
|
||||
"""
|
||||
Connects to the IMAP inbox, fetches new unread emails from the last 3 days,
|
||||
@@ -345,8 +387,22 @@ def pull_inbox(
|
||||
attributed to this user via ``process_document`` / ``convert_to_pdf``.
|
||||
gmail_apply_labels: Whether to apply Gmail-specific labels and stars to
|
||||
processed emails. Only relevant for Gmail hosts. Defaults to True.
|
||||
allowed_categories: List of file-type category keys to ingest (e.g.
|
||||
``["pdf", "office", "images"]``). ``None`` falls back to the
|
||||
global ``settings.imap_attachment_filter`` mapping.
|
||||
"""
|
||||
logger.info("Connecting to %s at %s:%s (SSL=%s)", mailbox_key, host, port, use_ssl)
|
||||
if allowed_categories is None:
|
||||
allowed_categories = _resolve_categories_for_profile(None)
|
||||
|
||||
effective_mime_types, effective_extensions = get_allowed_types_for_categories(allowed_categories)
|
||||
logger.info(
|
||||
"Connecting to %s at %s:%s (SSL=%s) — categories: %s",
|
||||
mailbox_key,
|
||||
host,
|
||||
port,
|
||||
use_ssl,
|
||||
allowed_categories,
|
||||
)
|
||||
processed_emails = load_processed_emails()
|
||||
|
||||
try:
|
||||
@@ -405,9 +461,13 @@ def pull_inbox(
|
||||
logger.info("Skipping email %s in %s, already labeled 'Ingested'.", msg_id, mailbox_key)
|
||||
continue
|
||||
|
||||
# Process attachments (and convert non-PDF files).
|
||||
# We call the function without assigning its return value since it is not used.
|
||||
fetch_attachments_and_enqueue(email_message, owner_id=owner_id)
|
||||
# Process attachments using the resolved mime types / extensions.
|
||||
fetch_attachments_and_enqueue(
|
||||
email_message,
|
||||
owner_id=owner_id,
|
||||
effective_mime_types=effective_mime_types,
|
||||
effective_extensions=effective_extensions,
|
||||
)
|
||||
|
||||
if settings.imap_readonly_mode:
|
||||
logger.info("Readonly mode: skipping mailbox modifications for %s in %s", msg_id, mailbox_key)
|
||||
@@ -436,27 +496,23 @@ def pull_inbox(
|
||||
logger.exception("Error pulling mailbox %s: %s", mailbox_key, e)
|
||||
|
||||
|
||||
def fetch_attachments_and_enqueue(email_message, owner_id: str | None = None):
|
||||
def fetch_attachments_and_enqueue(
|
||||
email_message,
|
||||
owner_id: str | None = None,
|
||||
effective_mime_types: frozenset[str] | None = None,
|
||||
effective_extensions: frozenset[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Extracts attachments from the email and processes only allowed file types.
|
||||
|
||||
Files are accepted if either:
|
||||
1. They have a MIME type from the ALLOWED_MIME_TYPES set, OR
|
||||
2. They have a '.pdf' file extension (regardless of MIME type)
|
||||
The caller is responsible for computing ``effective_mime_types`` and
|
||||
``effective_extensions`` from the relevant :class:`ImapIngestionProfile` (or
|
||||
the global default) via :func:`app.utils.allowed_types.get_allowed_types_for_categories`
|
||||
before calling this function. ``pull_inbox`` does this automatically.
|
||||
|
||||
Allowed file types include:
|
||||
- PDF: application/pdf or *.pdf extension
|
||||
- Microsoft Office files:
|
||||
- Word: application/msword,
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
- Excel: application/vnd.ms-excel,
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
- PowerPoint: application/vnd.ms-powerpoint,
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
- Other meaningful attachments:
|
||||
- Plain text: text/plain
|
||||
- CSV: text/csv
|
||||
- Rich Text Format: application/rtf, text/rtf
|
||||
If either set is ``None`` the function falls back to the default category list
|
||||
so the function still works correctly when called directly in tests or from
|
||||
other contexts.
|
||||
|
||||
If the attachment is a PDF (by extension or MIME type), it is enqueued for upload;
|
||||
any other allowed file is enqueued for conversion to PDF.
|
||||
@@ -465,9 +521,14 @@ def fetch_attachments_and_enqueue(email_message, owner_id: str | None = None):
|
||||
email_message: The parsed email message to extract attachments from.
|
||||
owner_id: Optional user identifier forwarded to ``process_document`` /
|
||||
``convert_to_pdf`` for multi-tenant attribution.
|
||||
effective_mime_types: Pre-computed frozenset of allowed MIME type strings.
|
||||
effective_extensions: Pre-computed frozenset of allowed file extension strings.
|
||||
|
||||
Returns True if at least one allowed attachment was processed.
|
||||
"""
|
||||
if effective_mime_types is None or effective_extensions is None:
|
||||
effective_mime_types, effective_extensions = get_allowed_types_for_categories(DEFAULT_CATEGORIES)
|
||||
|
||||
has_attachment = False
|
||||
for part in email_message.walk():
|
||||
if part.get_content_maintype() == "multipart":
|
||||
@@ -482,9 +543,15 @@ def fetch_attachments_and_enqueue(email_message, owner_id: str | None = None):
|
||||
|
||||
mime_type = part.get_content_type()
|
||||
file_ext = os.path.splitext(filename)[1].lower()
|
||||
|
||||
# Accept file if it has an allowed MIME type, an allowed extension, OR is a PDF by extension
|
||||
if mime_type not in ALLOWED_MIME_TYPES and file_ext not in ALLOWED_EXTENSIONS and not is_pdf_by_extension:
|
||||
logger.info("Skipping attachment %s with MIME type %s", filename, mime_type)
|
||||
if mime_type not in effective_mime_types and file_ext not in effective_extensions and not is_pdf_by_extension:
|
||||
logger.info(
|
||||
"Skipping attachment %s (MIME: %s, ext: %s) — not in effective allowed set",
|
||||
filename,
|
||||
mime_type,
|
||||
file_ext,
|
||||
)
|
||||
continue
|
||||
|
||||
file_path = os.path.join(settings.workdir, filename)
|
||||
@@ -495,7 +562,7 @@ def fetch_attachments_and_enqueue(email_message, owner_id: str | None = None):
|
||||
if mime_type == "application/pdf" or is_pdf_by_extension:
|
||||
process_document.delay(file_path, owner_id=owner_id)
|
||||
logger.info("Enqueued PDF for upload: %s (MIME: %s)", filename, mime_type)
|
||||
elif mime_type in ALLOWED_MIME_TYPES:
|
||||
elif mime_type in effective_mime_types:
|
||||
# Other allowed files are sent for conversion
|
||||
convert_to_pdf.delay(file_path, owner_id=owner_id)
|
||||
logger.info("Enqueued file for conversion to PDF: %s", filename)
|
||||
|
||||
+61
-12
@@ -12,6 +12,7 @@ from app.tasks.upload_to_dropbox import upload_to_dropbox
|
||||
from app.tasks.upload_to_email import upload_to_email
|
||||
from app.tasks.upload_to_ftp import upload_to_ftp
|
||||
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
||||
from app.tasks.upload_to_icloud import upload_to_icloud
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless
|
||||
@@ -25,18 +26,32 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _should_upload_to_dropbox():
|
||||
return bool(settings.dropbox_app_key and settings.dropbox_app_secret and settings.dropbox_refresh_token)
|
||||
return bool(
|
||||
getattr(settings, "dropbox_enabled", True)
|
||||
and settings.dropbox_app_key
|
||||
and settings.dropbox_app_secret
|
||||
and settings.dropbox_refresh_token
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_nextcloud():
|
||||
return bool(settings.nextcloud_upload_url and settings.nextcloud_username and settings.nextcloud_password)
|
||||
return bool(
|
||||
getattr(settings, "nextcloud_enabled", True)
|
||||
and settings.nextcloud_upload_url
|
||||
and settings.nextcloud_username
|
||||
and settings.nextcloud_password
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_paperless():
|
||||
return bool(settings.paperless_ngx_api_token and settings.paperless_host)
|
||||
return bool(
|
||||
getattr(settings, "paperless_enabled", True) and settings.paperless_ngx_api_token and settings.paperless_host
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_google_drive():
|
||||
if not getattr(settings, "google_drive_enabled", True):
|
||||
return False
|
||||
# Check for OAuth configuration
|
||||
if getattr(settings, "google_drive_use_oauth", False):
|
||||
return bool(
|
||||
@@ -51,20 +66,33 @@ def _should_upload_to_google_drive():
|
||||
|
||||
|
||||
def _should_upload_to_webdav():
|
||||
return bool(settings.webdav_url and settings.webdav_username and settings.webdav_password)
|
||||
return bool(
|
||||
getattr(settings, "webdav_enabled", True)
|
||||
and settings.webdav_url
|
||||
and settings.webdav_username
|
||||
and settings.webdav_password
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_ftp():
|
||||
return bool(settings.ftp_host and settings.ftp_username and settings.ftp_password)
|
||||
return bool(
|
||||
getattr(settings, "ftp_enabled", True) and settings.ftp_host and settings.ftp_username and settings.ftp_password
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_sftp():
|
||||
return bool(settings.sftp_host and settings.sftp_username and (settings.sftp_password or settings.sftp_private_key))
|
||||
return bool(
|
||||
getattr(settings, "sftp_enabled", True)
|
||||
and settings.sftp_host
|
||||
and settings.sftp_username
|
||||
and (settings.sftp_password or settings.sftp_private_key)
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_email():
|
||||
return bool(
|
||||
settings.dest_email_host
|
||||
getattr(settings, "dest_email_enabled", True)
|
||||
and settings.dest_email_host
|
||||
and settings.dest_email_username
|
||||
and settings.dest_email_password
|
||||
and settings.dest_email_default_recipient
|
||||
@@ -72,18 +100,32 @@ def _should_upload_to_email():
|
||||
|
||||
|
||||
def _should_upload_to_onedrive():
|
||||
return bool(settings.onedrive_client_id and settings.onedrive_client_secret and settings.onedrive_refresh_token)
|
||||
return bool(
|
||||
getattr(settings, "onedrive_enabled", True)
|
||||
and settings.onedrive_client_id
|
||||
and settings.onedrive_client_secret
|
||||
and settings.onedrive_refresh_token
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_s3():
|
||||
return bool(settings.s3_bucket_name and settings.aws_access_key_id and settings.aws_secret_access_key)
|
||||
return bool(
|
||||
getattr(settings, "s3_enabled", True)
|
||||
and settings.s3_bucket_name
|
||||
and settings.aws_access_key_id
|
||||
and settings.aws_secret_access_key
|
||||
)
|
||||
|
||||
|
||||
def _should_upload_to_icloud():
|
||||
return bool(getattr(settings, "icloud_enabled", True) and settings.icloud_username and settings.icloud_password)
|
||||
|
||||
|
||||
def get_configured_services_from_validator():
|
||||
"""
|
||||
Use the config validator to determine which services are configured properly.
|
||||
Use the config validator to determine which services are configured and enabled.
|
||||
Returns a dictionary with service names as keys and boolean values indicating
|
||||
whether they're properly configured.
|
||||
whether they're properly configured AND explicitly enabled.
|
||||
"""
|
||||
providers = get_provider_status()
|
||||
|
||||
@@ -98,12 +140,14 @@ def get_configured_services_from_validator():
|
||||
"Email": "email",
|
||||
"OneDrive": "onedrive",
|
||||
"S3 Storage": "s3",
|
||||
"iCloud Drive": "icloud",
|
||||
}
|
||||
|
||||
result = {}
|
||||
for provider_name, internal_name in service_map.items():
|
||||
if provider_name in providers:
|
||||
result[internal_name] = providers[provider_name].get("configured", False)
|
||||
provider = providers[provider_name]
|
||||
result[internal_name] = provider.get("configured", False) and provider.get("enabled", True)
|
||||
|
||||
return result
|
||||
|
||||
@@ -206,6 +250,11 @@ def send_to_all_destinations(self, file_path: str, use_validator=True, file_id:
|
||||
"should_upload": _should_upload_to_s3,
|
||||
"upload_func": upload_to_s3,
|
||||
},
|
||||
{
|
||||
"name": "icloud",
|
||||
"should_upload": _should_upload_to_icloud,
|
||||
"upload_func": upload_to_icloud,
|
||||
},
|
||||
]
|
||||
|
||||
# Optionally get configuration status from validator
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Upload files to Apple iCloud Drive via the pyicloud library.
|
||||
|
||||
This module uses the ``pyicloud`` library to authenticate with Apple's iCloud
|
||||
service and upload files to iCloud Drive. Because Apple does not offer a public
|
||||
REST API for iCloud Drive, this integration relies on the *unofficial*
|
||||
reverse-engineered protocol implemented by ``pyicloud``.
|
||||
|
||||
Requirements
|
||||
~~~~~~~~~~~~
|
||||
* An Apple ID with iCloud Drive enabled.
|
||||
* An **app-specific password** generated at https://appleid.apple.com (required
|
||||
when two-factor authentication is active – which is the default for all modern
|
||||
Apple IDs).
|
||||
* The ``pyicloud`` Python package (``pip install pyicloud``).
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
Set the following environment variables (or ``app/config.py`` fields):
|
||||
|
||||
* ``ICLOUD_USERNAME`` – Apple ID email address.
|
||||
* ``ICLOUD_PASSWORD`` – App-specific password.
|
||||
* ``ICLOUD_FOLDER`` – Target folder path inside iCloud Drive, using ``/`` as
|
||||
the separator (e.g. ``Documents/Uploads``). The folder is created
|
||||
automatically if it does not exist.
|
||||
* ``ICLOUD_COOKIE_DIRECTORY`` – (Optional) Directory for persisting session
|
||||
cookies so that re-authentication is avoided between task runs. Defaults to
|
||||
``~/.pyicloud``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from app.celery_app import celery
|
||||
from app.config import settings
|
||||
from app.tasks.retry_config import UploadTaskWithRetry
|
||||
from app.utils import log_task_progress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_icloud_api(
|
||||
username: str,
|
||||
password: str,
|
||||
cookie_directory: str | None = None,
|
||||
):
|
||||
"""Return an authenticated ``PyiCloudService`` instance.
|
||||
|
||||
Args:
|
||||
username: Apple ID email address.
|
||||
password: App-specific password.
|
||||
cookie_directory: Optional directory for session cookies.
|
||||
|
||||
Returns:
|
||||
An authenticated ``PyiCloudService`` instance.
|
||||
|
||||
Raises:
|
||||
ImportError: If ``pyicloud`` is not installed.
|
||||
ValueError: If authentication fails or 2FA is required interactively.
|
||||
"""
|
||||
from pyicloud import PyiCloudService # noqa: S404 – unofficial third-party iCloud client
|
||||
|
||||
kwargs: dict = {}
|
||||
if cookie_directory:
|
||||
kwargs["cookie_directory"] = cookie_directory
|
||||
|
||||
api = PyiCloudService(username, password, **kwargs)
|
||||
|
||||
# If 2SA/2FA is required the user must use an app-specific password instead.
|
||||
if api.requires_2sa or api.requires_2fa:
|
||||
raise ValueError(
|
||||
"iCloud account requires two-factor authentication. "
|
||||
"Please generate an app-specific password at https://appleid.apple.com "
|
||||
"and use it as ICLOUD_PASSWORD."
|
||||
)
|
||||
|
||||
return api
|
||||
|
||||
|
||||
def _navigate_to_folder(drive_root, folder_path: str):
|
||||
"""Navigate into (or create) the folder hierarchy described by *folder_path*.
|
||||
|
||||
Args:
|
||||
drive_root: The iCloud Drive root node (``api.drive``).
|
||||
folder_path: ``/``-separated path such as ``Documents/Uploads``.
|
||||
|
||||
Returns:
|
||||
The drive node representing the target folder.
|
||||
"""
|
||||
node = drive_root
|
||||
if not folder_path:
|
||||
return node
|
||||
|
||||
parts = [p for p in folder_path.strip("/").split("/") if p]
|
||||
for part in parts:
|
||||
children = {child.name: child for child in node.dir()}
|
||||
if part in children:
|
||||
node = children[part]
|
||||
else:
|
||||
# Create the missing folder
|
||||
node = node.mkdir(part)
|
||||
return node
|
||||
|
||||
|
||||
@celery.task(base=UploadTaskWithRetry, bind=True)
|
||||
def upload_to_icloud(self, file_path: str, file_id: int = None, folder_override: str = None):
|
||||
"""Upload a file to Apple iCloud Drive.
|
||||
|
||||
Args:
|
||||
file_path: Local path to the file to upload.
|
||||
file_id: Optional ``FileRecord.id`` for progress logging.
|
||||
folder_override: If provided, overrides the default ``ICLOUD_FOLDER``
|
||||
setting for this upload.
|
||||
"""
|
||||
task_id = self.request.id
|
||||
logger.info(f"[{task_id}] Starting iCloud Drive upload: {file_path}")
|
||||
log_task_progress(
|
||||
task_id,
|
||||
"upload_to_icloud",
|
||||
"in_progress",
|
||||
f"Uploading to iCloud Drive: {os.path.basename(file_path)}",
|
||||
file_id=file_id,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Validate inputs
|
||||
# ------------------------------------------------------------------
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"File not found: {file_path}"
|
||||
logger.error(f"[{task_id}] {error_msg}")
|
||||
log_task_progress(task_id, "upload_to_icloud", "failure", error_msg, file_id=file_id)
|
||||
raise FileNotFoundError(error_msg)
|
||||
|
||||
if not settings.icloud_username or not settings.icloud_password:
|
||||
error_msg = "iCloud credentials are not configured (ICLOUD_USERNAME / ICLOUD_PASSWORD)"
|
||||
logger.error(f"[{task_id}] {error_msg}")
|
||||
log_task_progress(task_id, "upload_to_icloud", "failure", error_msg, file_id=file_id)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
filename = os.path.basename(file_path)
|
||||
target_folder = folder_override if folder_override is not None else (settings.icloud_folder or "")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Authenticate & upload
|
||||
# ------------------------------------------------------------------
|
||||
try:
|
||||
api = _get_icloud_api(
|
||||
settings.icloud_username,
|
||||
settings.icloud_password,
|
||||
settings.icloud_cookie_directory,
|
||||
)
|
||||
|
||||
folder_node = _navigate_to_folder(api.drive, target_folder)
|
||||
|
||||
with open(file_path, "rb") as fh:
|
||||
folder_node.upload(fh)
|
||||
|
||||
logger.info(f"[{task_id}] Successfully uploaded {filename} to iCloud Drive folder '{target_folder}'")
|
||||
log_task_progress(
|
||||
task_id,
|
||||
"upload_to_icloud",
|
||||
"success",
|
||||
f"Uploaded to iCloud Drive: {filename}",
|
||||
file_id=file_id,
|
||||
)
|
||||
return {
|
||||
"status": "Completed",
|
||||
"file": file_path,
|
||||
"icloud_folder": target_folder or "/",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error uploading {filename} to iCloud Drive: {e}"
|
||||
logger.error(f"[{task_id}] {error_msg}")
|
||||
log_task_progress(task_id, "upload_to_icloud", "failure", error_msg, file_id=file_id)
|
||||
raise RuntimeError(error_msg) from e
|
||||
@@ -571,6 +571,37 @@ def _upload_rclone(file_path: str, cfg: dict[str, Any], creds: dict[str, Any], t
|
||||
return {"status": "Completed", "rclone_dest": dest}
|
||||
|
||||
|
||||
def _upload_icloud(file_path: str, cfg: dict[str, Any], creds: dict[str, Any], task_id: str) -> dict[str, Any]:
|
||||
"""Upload *file_path* to iCloud Drive using per-user credentials.
|
||||
|
||||
Expected *cfg* keys:
|
||||
* ``folder`` – target folder path inside iCloud Drive (e.g. ``Documents/Uploads``).
|
||||
* ``cookie_directory`` – (optional) path for session cookie persistence.
|
||||
|
||||
Expected *creds* keys:
|
||||
* ``username`` – Apple ID email address.
|
||||
* ``password`` – app-specific password.
|
||||
"""
|
||||
from app.tasks.upload_to_icloud import _get_icloud_api, _navigate_to_folder
|
||||
|
||||
username = creds.get("username") or ""
|
||||
password = creds.get("password") or ""
|
||||
folder = cfg.get("folder") or ""
|
||||
cookie_directory = cfg.get("cookie_directory") or None
|
||||
|
||||
if not username or not password:
|
||||
raise ValueError("iCloud integration is missing username or password in credentials")
|
||||
|
||||
api = _get_icloud_api(username, password, cookie_directory)
|
||||
folder_node = _navigate_to_folder(api.drive, folder)
|
||||
|
||||
with open(file_path, "rb") as fh:
|
||||
folder_node.upload(fh)
|
||||
|
||||
logger.info("[%s] iCloud Drive upload complete: folder=%s", task_id, folder or "/")
|
||||
return {"status": "Completed", "icloud_folder": folder or "/"}
|
||||
|
||||
|
||||
# Map IntegrationType → upload helper
|
||||
_UPLOAD_HANDLERS = {
|
||||
IntegrationType.DROPBOX: _upload_dropbox,
|
||||
@@ -584,6 +615,7 @@ _UPLOAD_HANDLERS = {
|
||||
IntegrationType.PAPERLESS: _upload_paperless,
|
||||
IntegrationType.EMAIL: _upload_email,
|
||||
IntegrationType.RCLONE: _upload_rclone,
|
||||
IntegrationType.ICLOUD: _upload_icloud,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -131,3 +131,157 @@ ALLOWED_EXTENSIONS: set[str] = {
|
||||
".md",
|
||||
".markdown",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fine-grained file-type categories used by IMAP ingestion profiles.
|
||||
# Each category groups related MIME types and extensions so that users can
|
||||
# enable/disable a logical collection of formats (e.g. "images") rather than
|
||||
# having to manage individual MIME strings.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FILE_TYPE_CATEGORIES: dict[str, dict] = {
|
||||
"pdf": {
|
||||
"label": "PDF",
|
||||
"description": "PDF documents (.pdf)",
|
||||
"mime_types": frozenset({"application/pdf"}),
|
||||
"extensions": frozenset({".pdf"}),
|
||||
},
|
||||
"office": {
|
||||
"label": "Microsoft Office",
|
||||
"description": "Word, Excel and PowerPoint files (.doc, .docx, .xls, .xlsx, .ppt, .pptx, …)",
|
||||
"mime_types": frozenset(
|
||||
{
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
"application/vnd.ms-word.document.macroEnabled.12",
|
||||
"application/vnd.ms-word.template.macroEnabled.12",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
}
|
||||
),
|
||||
"extensions": frozenset(
|
||||
{
|
||||
".doc",
|
||||
".docx",
|
||||
".docm",
|
||||
".dot",
|
||||
".dotx",
|
||||
".dotm",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".xlsm",
|
||||
".xlsb",
|
||||
".xlt",
|
||||
".xltx",
|
||||
".xlw",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".pptm",
|
||||
".pps",
|
||||
".ppsx",
|
||||
".pot",
|
||||
".potx",
|
||||
}
|
||||
),
|
||||
},
|
||||
"opendocument": {
|
||||
"label": "OpenDocument (LibreOffice)",
|
||||
"description": "LibreOffice / OpenOffice files (.odt, .ods, .odp, …)",
|
||||
"mime_types": frozenset(
|
||||
{
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.oasis.opendocument.graphics",
|
||||
"application/vnd.oasis.opendocument.formula",
|
||||
}
|
||||
),
|
||||
"extensions": frozenset({".odt", ".ods", ".odp", ".odg", ".odf"}),
|
||||
},
|
||||
"text": {
|
||||
"label": "Text & Data",
|
||||
"description": "Plain text, CSV and RTF files (.txt, .csv, .rtf)",
|
||||
"mime_types": frozenset(
|
||||
{
|
||||
"text/plain",
|
||||
"text/csv",
|
||||
"application/rtf",
|
||||
"text/rtf",
|
||||
}
|
||||
),
|
||||
"extensions": frozenset({".txt", ".csv", ".rtf"}),
|
||||
},
|
||||
"web": {
|
||||
"label": "Web & Markup",
|
||||
"description": "HTML and Markdown files (.html, .htm, .md, .markdown)",
|
||||
"mime_types": frozenset(
|
||||
{
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/x-markdown",
|
||||
}
|
||||
),
|
||||
"extensions": frozenset({".html", ".htm", ".md", ".markdown"}),
|
||||
},
|
||||
"images": {
|
||||
"label": "Images",
|
||||
"description": "Image files (.jpg, .png, .gif, .bmp, .tiff, .webp, .svg)",
|
||||
"mime_types": frozenset(
|
||||
{
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
}
|
||||
),
|
||||
"extensions": frozenset(
|
||||
{
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".webp",
|
||||
".svg",
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
# Default categories for the "documents only" built-in profile (no images)
|
||||
DEFAULT_CATEGORIES: list[str] = ["pdf", "office", "opendocument", "text", "web"]
|
||||
# All categories including images
|
||||
ALL_CATEGORIES: list[str] = ["pdf", "office", "opendocument", "text", "web", "images"]
|
||||
|
||||
|
||||
def get_allowed_types_for_categories(
|
||||
categories: list[str],
|
||||
) -> tuple[frozenset[str], frozenset[str]]:
|
||||
"""Return ``(mime_types, extensions)`` for the given category list.
|
||||
|
||||
Unknown category names are silently ignored so that future categories
|
||||
don't break existing profiles.
|
||||
"""
|
||||
mime_types: set[str] = set()
|
||||
extensions: set[str] = set()
|
||||
for cat in categories:
|
||||
info = FILE_TYPE_CATEGORIES.get(cat)
|
||||
if info:
|
||||
mime_types |= info["mime_types"]
|
||||
extensions |= info["extensions"]
|
||||
return frozenset(mime_types), frozenset(extensions)
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Comprehensive audit-event service for DocuElevate.
|
||||
|
||||
Provides helpers to **record** audit events (append-only database writes)
|
||||
and to optionally **forward** them to external SIEM systems.
|
||||
|
||||
Supported SIEM transports:
|
||||
* **Syslog** – RFC 5424 structured-data messages over UDP or TCP.
|
||||
* **HTTP** – JSON POST payloads compatible with Splunk HEC, Logstash
|
||||
HTTP input, Grafana Loki push API, and any generic webhook endpoint.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.middleware.audit_log import get_client_ip, get_username
|
||||
from app.models import AuditLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def record_event(
|
||||
db: Session,
|
||||
*,
|
||||
action: str,
|
||||
user: str = "system",
|
||||
resource_type: str | None = None,
|
||||
resource_id: str | None = None,
|
||||
ip_address: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
severity: str = "info",
|
||||
) -> AuditLog:
|
||||
"""Persist an audit event and optionally forward it to SIEM.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
action: Short action identifier (e.g. ``"login"``, ``"document.create"``).
|
||||
user: Username performing the action.
|
||||
resource_type: Category of the affected resource (``"document"``, ``"user"`` …).
|
||||
resource_id: Identifier of the affected resource.
|
||||
ip_address: Client IP address (``None`` when not applicable).
|
||||
details: Arbitrary key/value context serialised as JSON.
|
||||
severity: One of ``info``, ``warning``, ``error``, ``critical``.
|
||||
|
||||
Returns:
|
||||
The newly created :class:`AuditLog` row.
|
||||
"""
|
||||
details_json = json.dumps(details, default=str) if details else None
|
||||
|
||||
entry = AuditLog(
|
||||
user=user,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id) if resource_id is not None else None,
|
||||
ip_address=ip_address,
|
||||
details=details_json,
|
||||
severity=severity,
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
|
||||
# Fire-and-forget SIEM forwarding in a background thread so we never
|
||||
# block the request path.
|
||||
if settings.audit_siem_enabled:
|
||||
payload = _build_siem_payload(entry)
|
||||
thread = threading.Thread(target=_forward_to_siem, args=(payload,), daemon=True)
|
||||
thread.start()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def record_event_from_request(
|
||||
db: Session,
|
||||
request: Request,
|
||||
*,
|
||||
action: str,
|
||||
resource_type: str | None = None,
|
||||
resource_id: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
severity: str = "info",
|
||||
) -> AuditLog:
|
||||
"""Convenience wrapper that extracts user and IP from a :class:`Request`.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
request: The current HTTP request.
|
||||
action: Short action identifier.
|
||||
resource_type: Category of the affected resource.
|
||||
resource_id: Identifier of the affected resource.
|
||||
details: Arbitrary key/value context serialised as JSON.
|
||||
severity: One of ``info``, ``warning``, ``error``, ``critical``.
|
||||
|
||||
Returns:
|
||||
The newly created :class:`AuditLog` row.
|
||||
"""
|
||||
return record_event(
|
||||
db,
|
||||
action=action,
|
||||
user=get_username(request),
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
ip_address=get_client_ip(request),
|
||||
details=details,
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
|
||||
def query_events(
|
||||
db: Session,
|
||||
*,
|
||||
action: str | None = None,
|
||||
user: str | None = None,
|
||||
resource_type: str | None = None,
|
||||
severity: str | None = None,
|
||||
since: datetime | None = None,
|
||||
until: datetime | None = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
) -> list[AuditLog]:
|
||||
"""Query audit log entries with optional filtering.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
action: Filter by action string (exact match).
|
||||
user: Filter by username (exact match).
|
||||
resource_type: Filter by resource type (exact match).
|
||||
severity: Filter by severity level (exact match).
|
||||
since: Only events at or after this timestamp.
|
||||
until: Only events at or before this timestamp.
|
||||
limit: Maximum number of rows to return.
|
||||
offset: Number of rows to skip (for pagination).
|
||||
|
||||
Returns:
|
||||
List of :class:`AuditLog` rows ordered by *timestamp descending*.
|
||||
"""
|
||||
q = db.query(AuditLog)
|
||||
if action:
|
||||
q = q.filter(AuditLog.action == action)
|
||||
if user:
|
||||
q = q.filter(AuditLog.user == user)
|
||||
if resource_type:
|
||||
q = q.filter(AuditLog.resource_type == resource_type)
|
||||
if severity:
|
||||
q = q.filter(AuditLog.severity == severity)
|
||||
if since:
|
||||
q = q.filter(AuditLog.timestamp >= since)
|
||||
if until:
|
||||
q = q.filter(AuditLog.timestamp <= until)
|
||||
return q.order_by(AuditLog.timestamp.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
def count_events(
|
||||
db: Session,
|
||||
*,
|
||||
action: str | None = None,
|
||||
user: str | None = None,
|
||||
resource_type: str | None = None,
|
||||
severity: str | None = None,
|
||||
since: datetime | None = None,
|
||||
until: datetime | None = None,
|
||||
) -> int:
|
||||
"""Return the total count of events matching the given filters.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
action: Filter by action string.
|
||||
user: Filter by username.
|
||||
resource_type: Filter by resource type.
|
||||
severity: Filter by severity level.
|
||||
since: Only events at or after this timestamp.
|
||||
until: Only events at or before this timestamp.
|
||||
|
||||
Returns:
|
||||
Integer count.
|
||||
"""
|
||||
q = db.query(AuditLog)
|
||||
if action:
|
||||
q = q.filter(AuditLog.action == action)
|
||||
if user:
|
||||
q = q.filter(AuditLog.user == user)
|
||||
if resource_type:
|
||||
q = q.filter(AuditLog.resource_type == resource_type)
|
||||
if severity:
|
||||
q = q.filter(AuditLog.severity == severity)
|
||||
if since:
|
||||
q = q.filter(AuditLog.timestamp >= since)
|
||||
if until:
|
||||
q = q.filter(AuditLog.timestamp <= until)
|
||||
return q.count()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SIEM forwarding internals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SYSLOG_FACILITY_LOCAL0 = 16
|
||||
_SYSLOG_SEVERITY_MAP = {
|
||||
"info": 6,
|
||||
"warning": 4,
|
||||
"error": 3,
|
||||
"critical": 2,
|
||||
}
|
||||
|
||||
|
||||
def _build_siem_payload(entry: AuditLog) -> dict[str, Any]:
|
||||
"""Convert an :class:`AuditLog` row into a plain dict for SIEM delivery."""
|
||||
ts = entry.timestamp if entry.timestamp else datetime.now(timezone.utc)
|
||||
return {
|
||||
"id": entry.id,
|
||||
"timestamp": ts.isoformat(),
|
||||
"user": entry.user,
|
||||
"action": entry.action,
|
||||
"resource_type": entry.resource_type,
|
||||
"resource_id": entry.resource_id,
|
||||
"ip_address": entry.ip_address,
|
||||
"details": entry.details,
|
||||
"severity": entry.severity,
|
||||
"source": "docuelevate",
|
||||
}
|
||||
|
||||
|
||||
def _forward_to_siem(payload: dict[str, Any]) -> None:
|
||||
"""Route a SIEM payload to the configured transport."""
|
||||
transport = settings.audit_siem_transport.lower()
|
||||
try:
|
||||
if transport == "syslog":
|
||||
_send_syslog(payload)
|
||||
elif transport == "http":
|
||||
_send_http(payload)
|
||||
else:
|
||||
logger.warning("Unknown SIEM transport %r; skipping forwarding", transport)
|
||||
except Exception:
|
||||
logger.exception("Failed to forward audit event to SIEM (%s)", transport)
|
||||
|
||||
|
||||
def _send_syslog(payload: dict[str, Any]) -> None:
|
||||
"""Send a RFC 5424 syslog message to the configured receiver."""
|
||||
severity_num = _SYSLOG_SEVERITY_MAP.get(payload.get("severity", "info"), 6)
|
||||
priority = _SYSLOG_FACILITY_LOCAL0 * 8 + severity_num
|
||||
ts = payload.get("timestamp", datetime.now(timezone.utc).isoformat())
|
||||
hostname = socket.gethostname()
|
||||
app_name = "docuelevate"
|
||||
msg_id = payload.get("action", "-")
|
||||
|
||||
# Structured data (SD) element with key event fields.
|
||||
sd = (
|
||||
f'[docuelevate@0 user="{payload.get("user", "-")}" '
|
||||
f'action="{payload.get("action", "-")}" '
|
||||
f'resource_type="{payload.get("resource_type", "-")}" '
|
||||
f'resource_id="{payload.get("resource_id", "-")}" '
|
||||
f'ip="{payload.get("ip_address", "-")}"]'
|
||||
)
|
||||
message = json.dumps(payload, default=str)
|
||||
syslog_msg = f"<{priority}>1 {ts} {hostname} {app_name} - {msg_id} {sd} {message}"
|
||||
|
||||
proto = settings.audit_siem_syslog_protocol.lower()
|
||||
host = settings.audit_siem_syslog_host
|
||||
port = settings.audit_siem_syslog_port
|
||||
|
||||
if proto == "tcp":
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.connect((host, port))
|
||||
sock.sendall(syslog_msg.encode("utf-8"))
|
||||
else:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.sendto(syslog_msg.encode("utf-8"), (host, port))
|
||||
|
||||
logger.debug("Syslog audit event sent to %s:%s (%s)", host, port, proto)
|
||||
|
||||
|
||||
def _send_http(payload: dict[str, Any]) -> None:
|
||||
"""POST a JSON audit event to the configured HTTP endpoint."""
|
||||
url = settings.audit_siem_http_url
|
||||
if not url:
|
||||
logger.warning("SIEM HTTP URL not configured; skipping HTTP forwarding")
|
||||
return
|
||||
|
||||
headers: dict[str, str] = {"Content-Type": "application/json"}
|
||||
token = settings.audit_siem_http_token
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
# Parse custom headers (comma-separated "Key:Value" pairs).
|
||||
# Reject headers that could override security-critical ones already set,
|
||||
# and validate that header names contain only RFC 7230 token characters.
|
||||
_PROTECTED_HEADERS = {"authorization", "content-type", "host"}
|
||||
_VALID_HEADER_NAME = re.compile(r"^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$")
|
||||
raw_custom = settings.audit_siem_http_custom_headers
|
||||
if raw_custom:
|
||||
for raw_pair in raw_custom.split(","):
|
||||
pair = raw_pair.strip()
|
||||
if ":" in pair:
|
||||
k, _, v = pair.partition(":")
|
||||
name = k.strip()
|
||||
if not name or not _VALID_HEADER_NAME.match(name):
|
||||
logger.warning("Skipping invalid SIEM custom header name: %r", name)
|
||||
continue
|
||||
if name.lower() in _PROTECTED_HEADERS:
|
||||
logger.warning("Skipping protected SIEM custom header: %r", name)
|
||||
continue
|
||||
headers[name] = v.strip()
|
||||
|
||||
# Wrap in Splunk HEC-style envelope when URL contains ``/services/collector``.
|
||||
body: dict[str, Any]
|
||||
if "/services/collector" in url:
|
||||
body = {"event": payload, "sourcetype": "docuelevate:audit", "source": "docuelevate"}
|
||||
else:
|
||||
body = payload
|
||||
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(url, json=body, headers=headers)
|
||||
resp.raise_for_status()
|
||||
|
||||
logger.debug("HTTP audit event forwarded to %s (status %s)", url, resp.status_code)
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Compliance service for managing GDPR, HIPAA, and SOC2 compliance templates.
|
||||
|
||||
Provides pre-built compliance configurations that can be applied with one click
|
||||
to ensure the DocuElevate instance meets regulatory requirements.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import ComplianceTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-built compliance template definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
COMPLIANCE_TEMPLATES: dict[str, dict[str, Any]] = {
|
||||
"gdpr": {
|
||||
"display_name": "GDPR (General Data Protection Regulation)",
|
||||
"description": (
|
||||
"European Union regulation for data protection and privacy. "
|
||||
"Enforces data minimisation, encryption at rest, audit logging, "
|
||||
"and limits PII exposure in telemetry."
|
||||
),
|
||||
"settings": {
|
||||
"auth_enabled": "True",
|
||||
"sentry_send_default_pii": "False",
|
||||
"security_headers_enabled": "True",
|
||||
"security_header_hsts_enabled": "True",
|
||||
"security_header_csp_enabled": "True",
|
||||
"security_header_x_frame_options_enabled": "True",
|
||||
"enable_deduplication": "True",
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"key": "auth_enabled",
|
||||
"expected": "True",
|
||||
"label": "Authentication enabled",
|
||||
"description": "User authentication must be enabled to control access to personal data.",
|
||||
},
|
||||
{
|
||||
"key": "sentry_send_default_pii",
|
||||
"expected": "False",
|
||||
"label": "PII excluded from telemetry",
|
||||
"description": "Personally identifiable information must not be sent to external monitoring services.",
|
||||
},
|
||||
{
|
||||
"key": "security_headers_enabled",
|
||||
"expected": "True",
|
||||
"label": "Security headers enabled",
|
||||
"description": "HTTP security headers protect against common web vulnerabilities.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_hsts_enabled",
|
||||
"expected": "True",
|
||||
"label": "HSTS enabled",
|
||||
"description": "HTTP Strict Transport Security ensures encrypted connections.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_csp_enabled",
|
||||
"expected": "True",
|
||||
"label": "Content Security Policy enabled",
|
||||
"description": "CSP headers prevent cross-site scripting and data injection attacks.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_x_frame_options_enabled",
|
||||
"expected": "True",
|
||||
"label": "Clickjacking protection enabled",
|
||||
"description": "X-Frame-Options header prevents clickjacking attacks.",
|
||||
},
|
||||
{
|
||||
"key": "enable_deduplication",
|
||||
"expected": "True",
|
||||
"label": "Deduplication enabled",
|
||||
"description": "Data minimisation: avoid storing duplicate documents.",
|
||||
},
|
||||
],
|
||||
},
|
||||
"hipaa": {
|
||||
"display_name": "HIPAA (Health Insurance Portability and Accountability Act)",
|
||||
"description": (
|
||||
"United States regulation for protecting health information. "
|
||||
"Requires strong access controls, audit trails, encryption, "
|
||||
"and strict session management."
|
||||
),
|
||||
"settings": {
|
||||
"auth_enabled": "True",
|
||||
"multi_user_enabled": "True",
|
||||
"sentry_send_default_pii": "False",
|
||||
"security_headers_enabled": "True",
|
||||
"security_header_hsts_enabled": "True",
|
||||
"security_header_csp_enabled": "True",
|
||||
"security_header_x_frame_options_enabled": "True",
|
||||
"enable_deduplication": "True",
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"key": "auth_enabled",
|
||||
"expected": "True",
|
||||
"label": "Authentication enabled",
|
||||
"description": "Access controls are required to protect electronic Protected Health Information (ePHI).",
|
||||
},
|
||||
{
|
||||
"key": "multi_user_enabled",
|
||||
"expected": "True",
|
||||
"label": "Multi-user mode enabled",
|
||||
"description": "Individual user accounts required for access accountability.",
|
||||
},
|
||||
{
|
||||
"key": "sentry_send_default_pii",
|
||||
"expected": "False",
|
||||
"label": "PII excluded from telemetry",
|
||||
"description": "Protected Health Information must not be sent to external services.",
|
||||
},
|
||||
{
|
||||
"key": "security_headers_enabled",
|
||||
"expected": "True",
|
||||
"label": "Security headers enabled",
|
||||
"description": "Security headers protect ePHI during transmission.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_hsts_enabled",
|
||||
"expected": "True",
|
||||
"label": "HSTS enabled",
|
||||
"description": "Encrypted transport required for all ePHI transmissions.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_csp_enabled",
|
||||
"expected": "True",
|
||||
"label": "Content Security Policy enabled",
|
||||
"description": "CSP prevents injection attacks that could expose ePHI.",
|
||||
},
|
||||
{
|
||||
"key": "security_header_x_frame_options_enabled",
|
||||
"expected": "True",
|
||||
"label": "Clickjacking protection enabled",
|
||||
"description": "Prevents embedding the application in unauthorized frames.",
|
||||
},
|
||||
{
|
||||
"key": "enable_deduplication",
|
||||
"expected": "True",
|
||||
"label": "Deduplication enabled",
|
||||
"description": "Minimise data footprint for ePHI.",
|
||||
},
|
||||
],
|
||||
},
|
||||
"soc2": {
|
||||
"display_name": "SOC 2 (Service Organization Control 2)",
|
||||
"description": (
|
||||
"Trust Service Criteria framework for service organisations. "
|
||||
"Focuses on security, availability, processing integrity, "
|
||||
"confidentiality, and privacy."
|
||||
),
|
||||
"settings": {
|
||||
"auth_enabled": "True",
|
||||
"multi_user_enabled": "True",
|
||||
"sentry_send_default_pii": "False",
|
||||
"security_headers_enabled": "True",
|
||||
"security_header_hsts_enabled": "True",
|
||||
"security_header_csp_enabled": "True",
|
||||
"security_header_x_frame_options_enabled": "True",
|
||||
"enable_deduplication": "True",
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"key": "auth_enabled",
|
||||
"expected": "True",
|
||||
"label": "Authentication enabled",
|
||||
"description": "Logical access controls required (CC6.1).",
|
||||
},
|
||||
{
|
||||
"key": "multi_user_enabled",
|
||||
"expected": "True",
|
||||
"label": "Multi-user mode enabled",
|
||||
"description": "Individual user accounts for access management (CC6.2).",
|
||||
},
|
||||
{
|
||||
"key": "sentry_send_default_pii",
|
||||
"expected": "False",
|
||||
"label": "PII excluded from telemetry",
|
||||
"description": "Confidential information must not leak to external services (CC6.7).",
|
||||
},
|
||||
{
|
||||
"key": "security_headers_enabled",
|
||||
"expected": "True",
|
||||
"label": "Security headers enabled",
|
||||
"description": "Protection against common web threats (CC6.6).",
|
||||
},
|
||||
{
|
||||
"key": "security_header_hsts_enabled",
|
||||
"expected": "True",
|
||||
"label": "HSTS enabled",
|
||||
"description": "Encrypted transport in transit (CC6.7).",
|
||||
},
|
||||
{
|
||||
"key": "security_header_csp_enabled",
|
||||
"expected": "True",
|
||||
"label": "Content Security Policy enabled",
|
||||
"description": "Application-level security controls (CC6.6).",
|
||||
},
|
||||
{
|
||||
"key": "security_header_x_frame_options_enabled",
|
||||
"expected": "True",
|
||||
"label": "Clickjacking protection enabled",
|
||||
"description": "UI redress attack prevention (CC6.6).",
|
||||
},
|
||||
{
|
||||
"key": "enable_deduplication",
|
||||
"expected": "True",
|
||||
"label": "Deduplication enabled",
|
||||
"description": "Data integrity through deduplication (PI1.1).",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def seed_compliance_templates(db: Session) -> None:
|
||||
"""Create or update the built-in compliance template rows.
|
||||
|
||||
Called once at application startup to ensure the ``compliance_templates``
|
||||
table always contains the latest definitions.
|
||||
"""
|
||||
for name, defn in COMPLIANCE_TEMPLATES.items():
|
||||
existing = db.query(ComplianceTemplate).filter(ComplianceTemplate.name == name).first()
|
||||
if existing is None:
|
||||
template = ComplianceTemplate(
|
||||
name=name,
|
||||
display_name=defn["display_name"],
|
||||
description=defn["description"],
|
||||
settings_json=json.dumps(defn["settings"]),
|
||||
enabled=False,
|
||||
status="not_applied",
|
||||
)
|
||||
db.add(template)
|
||||
logger.info(f"Seeded compliance template: {name}")
|
||||
else:
|
||||
# Update display_name and description if changed, but preserve user state
|
||||
existing.display_name = defn["display_name"]
|
||||
existing.description = defn["description"]
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.exception("Failed to seed compliance templates")
|
||||
|
||||
|
||||
def get_all_templates(db: Session) -> list[dict[str, Any]]:
|
||||
"""Return all compliance templates with their current status."""
|
||||
templates = db.query(ComplianceTemplate).order_by(ComplianceTemplate.name).all()
|
||||
result = []
|
||||
for t in templates:
|
||||
defn = COMPLIANCE_TEMPLATES.get(t.name, {})
|
||||
checks = defn.get("checks", [])
|
||||
result.append(
|
||||
{
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"display_name": t.display_name,
|
||||
"description": t.description,
|
||||
"enabled": t.enabled,
|
||||
"status": t.status,
|
||||
"applied_at": t.applied_at.isoformat() if t.applied_at else None,
|
||||
"applied_by": t.applied_by,
|
||||
"settings": json.loads(t.settings_json) if t.settings_json else {},
|
||||
"checks": checks,
|
||||
"check_count": len(checks),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_template_by_name(db: Session, name: str) -> ComplianceTemplate | None:
|
||||
"""Retrieve a single compliance template by name."""
|
||||
return db.query(ComplianceTemplate).filter(ComplianceTemplate.name == name).first()
|
||||
|
||||
|
||||
def evaluate_template_status(db: Session, name: str) -> dict[str, Any]:
|
||||
"""Evaluate the compliance status of a template against live settings.
|
||||
|
||||
Returns a dict with ``status``, ``total``, ``passed``, ``failed``, and
|
||||
a list of individual ``check_results``.
|
||||
"""
|
||||
from app.config import settings as app_settings
|
||||
from app.utils.settings_service import get_all_settings_from_db
|
||||
|
||||
defn = COMPLIANCE_TEMPLATES.get(name)
|
||||
if defn is None:
|
||||
return {"status": "unknown", "total": 0, "passed": 0, "failed": 0, "check_results": []}
|
||||
|
||||
db_settings = get_all_settings_from_db(db)
|
||||
checks = defn.get("checks", [])
|
||||
results: list[dict[str, Any]] = []
|
||||
passed = 0
|
||||
|
||||
for check in checks:
|
||||
key = check["key"]
|
||||
expected = check["expected"]
|
||||
|
||||
# Resolve effective value: DB > config object
|
||||
if key in db_settings and db_settings[key] is not None:
|
||||
actual = str(db_settings[key])
|
||||
else:
|
||||
actual = str(getattr(app_settings, key, ""))
|
||||
|
||||
is_passing = actual.lower() == expected.lower()
|
||||
if is_passing:
|
||||
passed += 1
|
||||
|
||||
results.append(
|
||||
{
|
||||
"key": key,
|
||||
"label": check["label"],
|
||||
"description": check["description"],
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
"passing": is_passing,
|
||||
}
|
||||
)
|
||||
|
||||
total = len(checks)
|
||||
if passed == total:
|
||||
status = "compliant"
|
||||
elif passed > 0:
|
||||
status = "partial"
|
||||
else:
|
||||
status = "non_compliant"
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"total": total,
|
||||
"passed": passed,
|
||||
"failed": total - passed,
|
||||
"check_results": results,
|
||||
}
|
||||
|
||||
|
||||
def apply_template(db: Session, name: str, applied_by: str = "admin") -> dict[str, Any]:
|
||||
"""Apply a compliance template by writing its settings to the database.
|
||||
|
||||
Returns a summary of what was applied.
|
||||
"""
|
||||
from app.utils.settings_service import save_setting_to_db
|
||||
|
||||
defn = COMPLIANCE_TEMPLATES.get(name)
|
||||
if defn is None:
|
||||
return {"success": False, "error": f"Unknown template: {name}"}
|
||||
|
||||
template = get_template_by_name(db, name)
|
||||
if template is None:
|
||||
return {"success": False, "error": f"Template not found in database: {name}"}
|
||||
|
||||
applied_settings: dict[str, str] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for key, value in defn["settings"].items():
|
||||
try:
|
||||
save_setting_to_db(db, key, value, changed_by=f"compliance:{name}")
|
||||
applied_settings[key] = value
|
||||
except Exception as e:
|
||||
errors.append(f"{key}: {e}")
|
||||
logger.error(f"Failed to apply compliance setting {key}={value}: {e}")
|
||||
|
||||
# Update the template record
|
||||
now = datetime.now(timezone.utc)
|
||||
template.enabled = True
|
||||
template.settings_json = json.dumps(applied_settings)
|
||||
template.applied_at = now
|
||||
template.applied_by = applied_by
|
||||
|
||||
# Evaluate and store status
|
||||
eval_result = evaluate_template_status(db, name)
|
||||
template.status = eval_result["status"]
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.exception(f"Failed to update compliance template record: {name}")
|
||||
return {"success": False, "error": "Database commit failed"}
|
||||
|
||||
logger.info(f"Applied compliance template '{name}' by {applied_by}: {len(applied_settings)} settings written")
|
||||
|
||||
return {
|
||||
"success": len(errors) == 0,
|
||||
"template": name,
|
||||
"applied_settings": applied_settings,
|
||||
"errors": errors,
|
||||
"status": eval_result,
|
||||
}
|
||||
|
||||
|
||||
def get_compliance_summary(db: Session) -> dict[str, Any]:
|
||||
"""Return a high-level compliance dashboard summary across all templates."""
|
||||
templates = db.query(ComplianceTemplate).order_by(ComplianceTemplate.name).all()
|
||||
summary: list[dict[str, Any]] = []
|
||||
total_checks = 0
|
||||
total_passed = 0
|
||||
|
||||
for t in templates:
|
||||
eval_result = evaluate_template_status(db, t.name)
|
||||
total_checks += eval_result["total"]
|
||||
total_passed += eval_result["passed"]
|
||||
summary.append(
|
||||
{
|
||||
"name": t.name,
|
||||
"display_name": t.display_name,
|
||||
"enabled": t.enabled,
|
||||
"status": eval_result["status"],
|
||||
"total": eval_result["total"],
|
||||
"passed": eval_result["passed"],
|
||||
"failed": eval_result["failed"],
|
||||
"applied_at": t.applied_at.isoformat() if t.applied_at else None,
|
||||
"applied_by": t.applied_by,
|
||||
}
|
||||
)
|
||||
|
||||
overall = "compliant" if total_checks > 0 and total_passed == total_checks else "non_compliant"
|
||||
if 0 < total_passed < total_checks:
|
||||
overall = "partial"
|
||||
|
||||
return {
|
||||
"overall_status": overall,
|
||||
"total_checks": total_checks,
|
||||
"total_passed": total_passed,
|
||||
"total_failed": total_checks - total_passed,
|
||||
"templates": summary,
|
||||
}
|
||||
@@ -144,7 +144,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "dropbox_app_secret", None)
|
||||
and getattr(settings, "dropbox_refresh_token", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "dropbox_enabled", True),
|
||||
"description": "Upload files to Dropbox cloud storage",
|
||||
"details": {
|
||||
"folder": getattr(settings, "dropbox_folder", "Not set"),
|
||||
@@ -161,7 +161,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
"configured": bool(
|
||||
getattr(settings, "dest_email_host", None) and getattr(settings, "dest_email_default_recipient", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "dest_email_enabled", True),
|
||||
"description": "Send documents via email",
|
||||
"details": {
|
||||
"host": getattr(settings, "dest_email_host", "Not set"),
|
||||
@@ -183,7 +183,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "ftp_username", None)
|
||||
and getattr(settings, "ftp_password", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "ftp_enabled", True),
|
||||
"description": "Upload files to FTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, "ftp_host", "Not set"),
|
||||
@@ -214,7 +214,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
"name": "Google Drive",
|
||||
"icon": "fa-brands fa-google-drive",
|
||||
"configured": is_configured and bool(getattr(settings, "google_drive_folder_id", None)),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "google_drive_enabled", True),
|
||||
"description": "Store documents in Google Drive",
|
||||
"details": {
|
||||
"auth_type": "OAuth" if use_oauth else "Service Account",
|
||||
@@ -250,7 +250,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "nextcloud_username", None)
|
||||
and getattr(settings, "nextcloud_password", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "nextcloud_enabled", True),
|
||||
"description": "Store documents in NextCloud",
|
||||
"details": {
|
||||
"url": getattr(settings, "nextcloud_upload_url", "Not set"),
|
||||
@@ -270,7 +270,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "onedrive_client_secret", None)
|
||||
and getattr(settings, "onedrive_refresh_token", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "onedrive_enabled", True),
|
||||
"description": "Store documents in Microsoft OneDrive",
|
||||
"details": {
|
||||
"client_id": getattr(settings, "onedrive_client_id", "Not set"),
|
||||
@@ -288,7 +288,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
"configured": bool(
|
||||
getattr(settings, "paperless_host", None) and getattr(settings, "paperless_ngx_api_token", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "paperless_enabled", True),
|
||||
"description": "Document management system for digital archives",
|
||||
"details": {
|
||||
"host": getattr(settings, "paperless_host", "Not set"),
|
||||
@@ -305,7 +305,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "aws_access_key_id", None)
|
||||
and getattr(settings, "aws_secret_access_key", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "s3_enabled", True),
|
||||
"description": "Store documents in S3-compatible object storage",
|
||||
"details": {
|
||||
"bucket": getattr(settings, "s3_bucket_name", "Not set"),
|
||||
@@ -327,7 +327,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "sftp_username", None)
|
||||
and (getattr(settings, "sftp_password", None) or getattr(settings, "sftp_private_key", None))
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "sftp_enabled", True),
|
||||
"description": "Upload files to SFTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, "sftp_host", "Not set"),
|
||||
@@ -362,7 +362,7 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
and getattr(settings, "webdav_username", None)
|
||||
and getattr(settings, "webdav_password", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"enabled": getattr(settings, "webdav_enabled", True),
|
||||
"description": "Store documents on WebDAV servers",
|
||||
"details": {
|
||||
"url": getattr(settings, "webdav_url", "Not set"),
|
||||
@@ -373,4 +373,19 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
},
|
||||
}
|
||||
|
||||
# Check iCloud Drive configuration
|
||||
providers["iCloud Drive"] = {
|
||||
"name": "iCloud Drive",
|
||||
"icon": "fa-brands fa-apple",
|
||||
"configured": bool(getattr(settings, "icloud_username", None) and getattr(settings, "icloud_password", None)),
|
||||
"enabled": getattr(settings, "icloud_enabled", True),
|
||||
"description": "Store documents in Apple iCloud Drive",
|
||||
"details": {
|
||||
"username": getattr(settings, "icloud_username", "Not set"),
|
||||
"password": mask_sensitive_value(getattr(settings, "icloud_password", None)),
|
||||
"folder": getattr(settings, "icloud_folder", "Not set"),
|
||||
"cookie_directory": getattr(settings, "icloud_cookie_directory", "Not set"),
|
||||
},
|
||||
}
|
||||
|
||||
return providers
|
||||
|
||||
@@ -59,13 +59,43 @@ def validate_auth_config() -> list[str]:
|
||||
and getattr(settings, "authentik_config_url", None)
|
||||
)
|
||||
|
||||
if not using_simple_auth and not using_oidc:
|
||||
issues.append("Neither simple authentication nor OIDC are properly configured")
|
||||
# Check if any social login provider is enabled
|
||||
using_social_login = any(
|
||||
getattr(settings, f"social_auth_{p}_enabled", False) for p in ("google", "microsoft", "apple", "dropbox")
|
||||
)
|
||||
|
||||
if not using_simple_auth and not using_oidc and not using_social_login:
|
||||
issues.append("Neither simple authentication, OIDC, nor social login are properly configured")
|
||||
|
||||
# If using OIDC, check for provider name
|
||||
if using_oidc and not getattr(settings, "oauth_provider_name", None):
|
||||
issues.append("OAUTH_PROVIDER_NAME is not configured but OIDC is enabled")
|
||||
|
||||
# Validate individual social login provider configs
|
||||
if getattr(settings, "social_auth_google_enabled", False):
|
||||
if not getattr(settings, "social_auth_google_client_id", None):
|
||||
issues.append("SOCIAL_AUTH_GOOGLE_CLIENT_ID is required when Google login is enabled")
|
||||
if not getattr(settings, "social_auth_google_client_secret", None):
|
||||
issues.append("SOCIAL_AUTH_GOOGLE_CLIENT_SECRET is required when Google login is enabled")
|
||||
|
||||
if getattr(settings, "social_auth_microsoft_enabled", False):
|
||||
if not getattr(settings, "social_auth_microsoft_client_id", None):
|
||||
issues.append("SOCIAL_AUTH_MICROSOFT_CLIENT_ID is required when Microsoft login is enabled")
|
||||
if not getattr(settings, "social_auth_microsoft_client_secret", None):
|
||||
issues.append("SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET is required when Microsoft login is enabled")
|
||||
|
||||
if getattr(settings, "social_auth_apple_enabled", False):
|
||||
if not getattr(settings, "social_auth_apple_client_id", None):
|
||||
issues.append("SOCIAL_AUTH_APPLE_CLIENT_ID is required when Apple login is enabled")
|
||||
if not getattr(settings, "social_auth_apple_team_id", None):
|
||||
issues.append("SOCIAL_AUTH_APPLE_TEAM_ID is required when Apple login is enabled")
|
||||
|
||||
if getattr(settings, "social_auth_dropbox_enabled", False):
|
||||
if not getattr(settings, "social_auth_dropbox_client_id", None):
|
||||
issues.append("SOCIAL_AUTH_DROPBOX_CLIENT_ID is required when Dropbox login is enabled")
|
||||
if not getattr(settings, "social_auth_dropbox_client_secret", None):
|
||||
issues.append("SOCIAL_AUTH_DROPBOX_CLIENT_SECRET is required when Dropbox login is enabled")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
|
||||
@@ -32,8 +32,10 @@ _TABLE_ORDER = [
|
||||
"processing_logs",
|
||||
"application_settings",
|
||||
"settings_audit_log",
|
||||
"audit_logs",
|
||||
"saved_searches",
|
||||
"webhook_configs",
|
||||
"shared_links",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
"""Internationalization (i18n) and localization (l10n) utilities.
|
||||
|
||||
Provides a JSON-based translation system for the DocuElevate UI with:
|
||||
|
||||
* **31 supported languages** covering all major European languages plus ZH
|
||||
* Browser ``Accept-Language`` detection with cookie & user-profile persistence
|
||||
* AI-powered fallback translation via the configured LLM provider
|
||||
* Locale-aware date, number, and file-size formatting helpers
|
||||
* Jinja2 integration via a ``_()`` global function
|
||||
|
||||
Language resolution order:
|
||||
1. User profile ``preferred_language`` (persisted in DB)
|
||||
2. ``docuelevate_lang`` cookie
|
||||
3. ``Accept-Language`` HTTP header
|
||||
4. Default (``en``)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from starlette.requests import Request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Supported languages (ordered by priority)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SUPPORTED_LANGUAGES: list[dict[str, str]] = [
|
||||
# --- Tier 1: Primary European languages ---
|
||||
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
|
||||
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"},
|
||||
{"code": "fr", "name": "French", "native": "Français", "flag": "🇫🇷"},
|
||||
{"code": "es", "name": "Spanish", "native": "Español", "flag": "🇪🇸"},
|
||||
{"code": "it", "name": "Italian", "native": "Italiano", "flag": "🇮🇹"},
|
||||
{"code": "pt", "name": "Portuguese", "native": "Português", "flag": "🇵🇹"},
|
||||
# --- Tier 2: Western & Northern European ---
|
||||
{"code": "nl", "name": "Dutch", "native": "Nederlands", "flag": "🇳🇱"},
|
||||
{"code": "nb", "name": "Norwegian", "native": "Norsk", "flag": "🇳🇴"},
|
||||
{"code": "da", "name": "Danish", "native": "Dansk", "flag": "🇩🇰"},
|
||||
{"code": "sv", "name": "Swedish", "native": "Svenska", "flag": "🇸🇪"},
|
||||
{"code": "fi", "name": "Finnish", "native": "Suomi", "flag": "🇫🇮"},
|
||||
{"code": "is", "name": "Icelandic", "native": "Íslenska", "flag": "🇮🇸"},
|
||||
{"code": "ga", "name": "Irish", "native": "Gaeilge", "flag": "🇮🇪"},
|
||||
{"code": "lb", "name": "Luxembourgish", "native": "Lëtzebuergesch", "flag": "🇱🇺"},
|
||||
{"code": "ca", "name": "Catalan", "native": "Català", "flag": "🏴"},
|
||||
# --- Tier 3: Central & Eastern European ---
|
||||
{"code": "pl", "name": "Polish", "native": "Polski", "flag": "🇵🇱"},
|
||||
{"code": "cs", "name": "Czech", "native": "Čeština", "flag": "🇨🇿"},
|
||||
{"code": "sk", "name": "Slovak", "native": "Slovenčina", "flag": "🇸🇰"},
|
||||
{"code": "hu", "name": "Hungarian", "native": "Magyar", "flag": "🇭🇺"},
|
||||
{"code": "sl", "name": "Slovenian", "native": "Slovenščina", "flag": "🇸🇮"},
|
||||
{"code": "hr", "name": "Croatian", "native": "Hrvatski", "flag": "🇭🇷"},
|
||||
{"code": "ro", "name": "Romanian", "native": "Română", "flag": "🇷🇴"},
|
||||
{"code": "bg", "name": "Bulgarian", "native": "Български", "flag": "🇧🇬"},
|
||||
{"code": "el", "name": "Greek", "native": "Ελληνικά", "flag": "🇬🇷"},
|
||||
{"code": "et", "name": "Estonian", "native": "Eesti", "flag": "🇪🇪"},
|
||||
{"code": "lv", "name": "Latvian", "native": "Latviešu", "flag": "🇱🇻"},
|
||||
{"code": "lt", "name": "Lithuanian", "native": "Lietuvių", "flag": "🇱🇹"},
|
||||
# --- Tier 4: Non-EU European & Other ---
|
||||
{"code": "tr", "name": "Turkish", "native": "Türkçe", "flag": "🇹🇷"},
|
||||
{"code": "uk", "name": "Ukrainian", "native": "Українська", "flag": "🇺🇦"},
|
||||
{"code": "ru", "name": "Russian", "native": "Русский", "flag": "🇷🇺"},
|
||||
{"code": "zh", "name": "Chinese", "native": "中文", "flag": "🇨🇳"},
|
||||
]
|
||||
|
||||
SUPPORTED_LANGUAGE_CODES: set[str] = {lang["code"] for lang in SUPPORTED_LANGUAGES}
|
||||
DEFAULT_LANGUAGE = "en"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation file loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TRANSLATIONS_DIR = Path(__file__).resolve().parent.parent.parent / "frontend" / "translations"
|
||||
_translation_cache: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
def _load_translations(locale: str) -> dict[str, str]:
|
||||
"""Load the translation JSON file for *locale*, with caching."""
|
||||
if locale in _translation_cache:
|
||||
return _translation_cache[locale]
|
||||
|
||||
filepath = _TRANSLATIONS_DIR / f"{locale}.json"
|
||||
if not filepath.is_file():
|
||||
logger.warning("Translation file not found for locale '%s'", locale)
|
||||
_translation_cache[locale] = {}
|
||||
return {}
|
||||
|
||||
try:
|
||||
data: dict[str, str] = json.loads(filepath.read_text(encoding="utf-8"))
|
||||
_translation_cache[locale] = data
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
logger.exception("Failed to load translations for '%s'", locale)
|
||||
_translation_cache[locale] = {}
|
||||
return {}
|
||||
|
||||
|
||||
def reload_translations() -> None:
|
||||
"""Clear the translation cache so files are re-read on next access."""
|
||||
_translation_cache.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core translation function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def translate(key: str, locale: str | None = None, **kwargs: Any) -> str:
|
||||
"""Return the translated string for *key* in *locale*.
|
||||
|
||||
Falls back through:
|
||||
1. Requested *locale*
|
||||
2. English (``en``)
|
||||
3. The raw key itself (to keep the UI functional)
|
||||
|
||||
Positional placeholders ``{0}``, ``{1}`` or named placeholders
|
||||
``{name}`` in the translated string are interpolated via *kwargs*.
|
||||
"""
|
||||
locale = locale if locale and locale in SUPPORTED_LANGUAGE_CODES else DEFAULT_LANGUAGE
|
||||
|
||||
translations = _load_translations(locale)
|
||||
value = translations.get(key)
|
||||
|
||||
# Fallback to English
|
||||
if value is None and locale != DEFAULT_LANGUAGE:
|
||||
en_translations = _load_translations(DEFAULT_LANGUAGE)
|
||||
value = en_translations.get(key)
|
||||
|
||||
# Fallback to key itself
|
||||
if value is None:
|
||||
value = key
|
||||
|
||||
if kwargs:
|
||||
try:
|
||||
value = value.format(**kwargs)
|
||||
except (KeyError, IndexError):
|
||||
pass # Return unformatted string rather than crash
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI fallback translation (best-effort, non-blocking)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ai_translation_cache: dict[tuple[str, str], str] = {}
|
||||
|
||||
|
||||
def translate_with_ai_fallback(text: str, target_locale: str) -> str:
|
||||
"""Translate *text* using the configured AI provider as a fallback.
|
||||
|
||||
Returns the original *text* unchanged when:
|
||||
* The target locale is English (source language)
|
||||
* The AI provider is unavailable or returns an error
|
||||
* The translation has already been cached
|
||||
|
||||
Results are cached in-memory for the lifetime of the process.
|
||||
"""
|
||||
if target_locale == DEFAULT_LANGUAGE or target_locale not in SUPPORTED_LANGUAGE_CODES:
|
||||
return text
|
||||
|
||||
cache_key = (text, target_locale)
|
||||
if cache_key in _ai_translation_cache:
|
||||
return _ai_translation_cache[cache_key]
|
||||
|
||||
target_name = next(
|
||||
(lang["name"] for lang in SUPPORTED_LANGUAGES if lang["code"] == target_locale),
|
||||
target_locale,
|
||||
)
|
||||
|
||||
try:
|
||||
from litellm import completion # type: ignore[import-untyped]
|
||||
|
||||
from app.config import settings
|
||||
|
||||
model = getattr(settings, "ai_model", None) or getattr(settings, "openai_model", "gpt-4o-mini")
|
||||
response = completion(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You are a professional translator. Translate the following UI text "
|
||||
f"from English to {target_name}. Return ONLY the translated text, "
|
||||
f"nothing else. Keep any HTML tags, placeholders like {{name}}, "
|
||||
f"and special characters intact."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
max_tokens=256,
|
||||
temperature=0.1,
|
||||
)
|
||||
translated = response.choices[0].message.content.strip()
|
||||
_ai_translation_cache[cache_key] = translated
|
||||
return translated
|
||||
except Exception:
|
||||
logger.debug("AI fallback translation failed for '%s' → %s", text[:50], target_locale)
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def detect_language(request: Request) -> str:
|
||||
"""Determine the preferred UI language from the request context.
|
||||
|
||||
Resolution order:
|
||||
1. ``preferred_language`` stored in the user session
|
||||
2. ``docuelevate_lang`` cookie
|
||||
3. ``Accept-Language`` HTTP header (best match)
|
||||
4. Default → ``en``
|
||||
"""
|
||||
# 1. User session preference
|
||||
if hasattr(request, "session"):
|
||||
session_lang = request.session.get("preferred_language")
|
||||
if isinstance(session_lang, str) and session_lang in SUPPORTED_LANGUAGE_CODES:
|
||||
return session_lang
|
||||
|
||||
# 2. Cookie
|
||||
if hasattr(request, "cookies"):
|
||||
cookie_lang = request.cookies.get("docuelevate_lang")
|
||||
if isinstance(cookie_lang, str) and cookie_lang in SUPPORTED_LANGUAGE_CODES:
|
||||
return cookie_lang
|
||||
|
||||
# 3. Accept-Language header
|
||||
accept = ""
|
||||
if hasattr(request, "headers"):
|
||||
accept = request.headers.get("accept-language", "")
|
||||
lang = _parse_accept_language(accept)
|
||||
if lang:
|
||||
return lang
|
||||
|
||||
return DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def _parse_accept_language(header: str) -> str | None:
|
||||
"""Extract the best matching language from an ``Accept-Language`` header.
|
||||
|
||||
Parses quality values and returns the highest-priority match among
|
||||
:data:`SUPPORTED_LANGUAGE_CODES`, or ``None`` if nothing matches.
|
||||
"""
|
||||
if not header:
|
||||
return None
|
||||
|
||||
entries: list[tuple[float, str]] = []
|
||||
for raw_part in header.split(","):
|
||||
part = raw_part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if ";q=" in part:
|
||||
lang_tag, _, q_str = part.partition(";q=")
|
||||
try:
|
||||
quality = float(q_str.strip())
|
||||
except ValueError:
|
||||
quality = 0.0
|
||||
else:
|
||||
lang_tag = part
|
||||
quality = 1.0
|
||||
entries.append((quality, lang_tag.strip().lower()))
|
||||
|
||||
# Sort by quality descending
|
||||
entries.sort(key=lambda e: e[0], reverse=True)
|
||||
|
||||
for _quality, tag in entries:
|
||||
# Try exact match first (e.g., "de", "zh")
|
||||
code = tag.split("-")[0]
|
||||
if code in SUPPORTED_LANGUAGE_CODES:
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Localization helpers (l10n)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Locale-specific formatting rules for date/number display
|
||||
_LOCALE_FORMATS: dict[str, dict[str, Any]] = {
|
||||
"en": {
|
||||
"date": "%B %d, %Y",
|
||||
"date_short": "%m/%d/%Y",
|
||||
"datetime": "%B %d, %Y %I:%M %p",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"de": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"fr": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u202f",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"es": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"it": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"pt": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"nl": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d-%m-%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"nb": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"da": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sv": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%Y-%m-%d",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"fi": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"is": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ga": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"lb": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ca": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"pl": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"cs": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sk": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"hu": {
|
||||
"date": "%Y. %B %d.",
|
||||
"date_short": "%Y.%m.%d.",
|
||||
"datetime": "%Y. %B %d. %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sl": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"hr": {
|
||||
"date": "%d. %B %Y.",
|
||||
"date_short": "%d.%m.%Y.",
|
||||
"datetime": "%d. %B %Y. %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ro": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"bg": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"el": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"et": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"lv": {
|
||||
"date": "%Y. gada %d. %B",
|
||||
"date_short": "%d.%m.%Y.",
|
||||
"datetime": "%Y. gada %d. %B %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"lt": {
|
||||
"date": "%Y m. %B %d d.",
|
||||
"date_short": "%Y-%m-%d",
|
||||
"datetime": "%Y m. %B %d d. %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"tr": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"uk": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"zh": {
|
||||
"date": "%Y年%m月%d日",
|
||||
"date_short": "%Y/%m/%d",
|
||||
"datetime": "%Y年%m月%d日 %H:%M",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"ru": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def format_date(value: date | datetime | None, locale: str = DEFAULT_LANGUAGE, short: bool = False) -> str:
|
||||
"""Format a date/datetime value according to the locale conventions."""
|
||||
if value is None:
|
||||
return ""
|
||||
fmt_key = "date_short" if short else "date"
|
||||
fmt = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])[fmt_key]
|
||||
return value.strftime(fmt)
|
||||
|
||||
|
||||
def format_datetime(value: datetime | None, locale: str = DEFAULT_LANGUAGE) -> str:
|
||||
"""Format a datetime value according to the locale conventions."""
|
||||
if value is None:
|
||||
return ""
|
||||
fmt = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])["datetime"]
|
||||
return value.strftime(fmt)
|
||||
|
||||
|
||||
def format_number(value: int | float, locale: str = DEFAULT_LANGUAGE) -> str:
|
||||
"""Format a number with locale-appropriate thousand separators."""
|
||||
lf = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])
|
||||
if isinstance(value, float):
|
||||
int_part, _, dec_part = f"{value:,.2f}".partition(".")
|
||||
formatted_int = int_part.replace(",", lf["thousands_sep"])
|
||||
return f"{formatted_int}{lf['decimal_sep']}{dec_part}"
|
||||
return f"{value:,}".replace(",", lf["thousands_sep"])
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_language_info(code: str) -> dict[str, str] | None:
|
||||
"""Return the metadata dict for a supported language code, or ``None``."""
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if lang["code"] == code:
|
||||
return lang
|
||||
return None
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Push notification sender for the DocuElevate mobile app.
|
||||
|
||||
Uses the **Expo Push Notification** service to deliver notifications to both
|
||||
iOS (via APNs) and Android (via FCM) without requiring server-side APNs keys
|
||||
or FCM credentials. The mobile app obtains an ``ExponentPushToken[…]`` at
|
||||
startup and registers it with the backend via the mobile API.
|
||||
|
||||
Reference: https://docs.expo.dev/push-notifications/sending-notifications/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.database import SessionLocal
|
||||
from app.models import MobileDevice
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send"
|
||||
|
||||
# Maximum tokens per batch request (Expo limit).
|
||||
_EXPO_BATCH_LIMIT = 100
|
||||
|
||||
|
||||
def send_expo_push_notification(
|
||||
tokens: list[str],
|
||||
title: str,
|
||||
body: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
sound: str = "default",
|
||||
badge: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Send a push notification to one or more Expo push tokens.
|
||||
|
||||
Args:
|
||||
tokens: List of Expo push tokens (``ExponentPushToken[…]``).
|
||||
title: Notification title shown in the system tray.
|
||||
body: Notification body text.
|
||||
data: Optional JSON-serialisable dict attached to the notification
|
||||
(available in the app via ``notification.request.content.data``).
|
||||
sound: Notification sound. Use ``"default"`` or ``None`` for silent.
|
||||
badge: iOS badge count. Pass ``0`` to clear.
|
||||
|
||||
Returns:
|
||||
List of Expo push receipt dicts (one per token).
|
||||
"""
|
||||
if not tokens:
|
||||
return []
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
# Send in batches to stay within Expo's per-request limit.
|
||||
for i in range(0, len(tokens), _EXPO_BATCH_LIMIT):
|
||||
batch = tokens[i : i + _EXPO_BATCH_LIMIT]
|
||||
messages = []
|
||||
for token in batch:
|
||||
msg: dict[str, Any] = {
|
||||
"to": token,
|
||||
"title": title,
|
||||
"body": body,
|
||||
"sound": sound,
|
||||
}
|
||||
if data:
|
||||
msg["data"] = data
|
||||
if badge is not None:
|
||||
msg["badge"] = badge
|
||||
messages.append(msg)
|
||||
|
||||
try:
|
||||
resp = httpx.post(
|
||||
EXPO_PUSH_URL,
|
||||
json=messages,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
batch_results = payload.get("data", [])
|
||||
results.extend(batch_results)
|
||||
logger.debug("Expo push batch sent: %d tokens, %d results", len(batch), len(batch_results))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
logger.error("Expo push HTTP error: %s – %s", exc.response.status_code, exc.response.text)
|
||||
except Exception:
|
||||
logger.exception("Expo push notification failed for batch starting at index %d", i)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def send_push_to_owner(
|
||||
owner_id: str,
|
||||
title: str,
|
||||
body: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Look up all active push tokens for *owner_id* and send them a notification.
|
||||
|
||||
This function is safe to call from Celery task workers. Database errors
|
||||
and push failures are logged but never raised so that the caller task is
|
||||
not retried due to a notification failure.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
devices = (
|
||||
db.query(MobileDevice)
|
||||
.filter(
|
||||
MobileDevice.owner_id == owner_id,
|
||||
MobileDevice.is_active.is_(True),
|
||||
MobileDevice.push_token.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
tokens = [d.push_token for d in devices if d.push_token]
|
||||
except Exception:
|
||||
logger.exception("Failed to query mobile devices for owner_id=%s", owner_id)
|
||||
return
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if not tokens:
|
||||
logger.debug("No active push tokens for owner_id=%s", owner_id)
|
||||
return
|
||||
|
||||
logger.info("Sending push notification to %d device(s) for owner_id=%s", len(tokens), owner_id)
|
||||
send_expo_push_notification(tokens=tokens, title=title, body=body, data=data)
|
||||
@@ -182,6 +182,153 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
# Social Login Providers
|
||||
"social_auth_google_enabled": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Enable Google Sign-In. Requires SOCIAL_AUTH_GOOGLE_CLIENT_ID and "
|
||||
"SOCIAL_AUTH_GOOGLE_CLIENT_SECRET from the Google Cloud Console."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
"help_link": "https://console.cloud.google.com/apis/credentials",
|
||||
"help_link_label": "Google Cloud Console",
|
||||
},
|
||||
"social_auth_google_client_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Google OAuth2 client ID from the Google Cloud Console.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_google_client_secret": {
|
||||
"category": "Social Login",
|
||||
"description": "Google OAuth2 client secret from the Google Cloud Console.",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_microsoft_enabled": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Enable Microsoft Sign-In (Azure AD / Microsoft Entra ID). Requires "
|
||||
"SOCIAL_AUTH_MICROSOFT_CLIENT_ID and SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET "
|
||||
"from Azure App Registrations."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
"help_link": "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade",
|
||||
"help_link_label": "Azure Portal",
|
||||
},
|
||||
"social_auth_microsoft_client_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Microsoft OAuth2 application (client) ID from Azure App Registrations.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_microsoft_client_secret": {
|
||||
"category": "Social Login",
|
||||
"description": "Microsoft OAuth2 client secret from Azure App Registrations.",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_microsoft_tenant": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Azure AD tenant ID or one of 'common', 'organizations', 'consumers'. "
|
||||
"Use 'common' to allow any Microsoft account. Use a specific GUID to "
|
||||
"restrict to a single organization."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_apple_enabled": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Enable Sign in with Apple. Requires an Apple Developer account with "
|
||||
"a Services ID configured for Sign in with Apple."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
"help_link": "https://developer.apple.com/account/resources/identifiers/list/serviceId",
|
||||
"help_link_label": "Apple Developer Portal",
|
||||
},
|
||||
"social_auth_apple_client_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Apple Services ID (e.g. com.example.docuelevate).",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_apple_team_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Apple Developer Team ID (10-character alphanumeric string).",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_apple_key_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Apple Sign-In private key ID from the Apple Developer Portal.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_apple_private_key": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Apple Sign-In private key (PEM format). Generate this in the Apple Developer Portal. "
|
||||
"Paste the entire key content including BEGIN/END headers."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_dropbox_enabled": {
|
||||
"category": "Social Login",
|
||||
"description": (
|
||||
"Enable Dropbox Sign-In. Uses the same Dropbox App you may already have "
|
||||
"configured for storage, or a separate one."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_dropbox_client_id": {
|
||||
"category": "Social Login",
|
||||
"description": "Dropbox OAuth2 App Key from the Dropbox App Console.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"social_auth_dropbox_client_secret": {
|
||||
"category": "Social Login",
|
||||
"description": "Dropbox OAuth2 App Secret from the Dropbox App Console.",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
# AI Services
|
||||
"openai_api_key": {
|
||||
"category": "AI Services",
|
||||
@@ -495,6 +642,14 @@ SETTING_METADATA = {
|
||||
"options": ["us", "eu"],
|
||||
},
|
||||
# Storage Providers - Dropbox
|
||||
"dropbox_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable Dropbox as an upload destination. When disabled, no documents will be sent to Dropbox even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dropbox_app_key": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Dropbox app key for OAuth authentication",
|
||||
@@ -528,6 +683,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - Nextcloud
|
||||
"nextcloud_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable Nextcloud as an upload destination. When disabled, no documents will be sent to Nextcloud even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"nextcloud_upload_url": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Nextcloud WebDAV upload URL",
|
||||
@@ -561,6 +724,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - Paperless-ngx
|
||||
"paperless_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable Paperless-ngx as an upload destination. When disabled, no documents will be sent to Paperless-ngx even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"paperless_ngx_api_token": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Paperless-ngx API authentication token",
|
||||
@@ -578,6 +749,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - Google Drive
|
||||
"google_drive_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable Google Drive as an upload destination. When disabled, no documents will be sent to Google Drive even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"google_drive_credentials_json": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Google Drive service account credentials JSON",
|
||||
@@ -635,6 +814,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - OneDrive
|
||||
"onedrive_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable OneDrive as an upload destination. When disabled, no documents will be sent to OneDrive even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"onedrive_client_id": {
|
||||
"category": "Storage Providers",
|
||||
"description": "OneDrive OAuth client ID",
|
||||
@@ -676,6 +863,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - WebDAV
|
||||
"webdav_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable WebDAV as an upload destination. When disabled, no documents will be sent to WebDAV even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"webdav_url": {
|
||||
"category": "Storage Providers",
|
||||
"description": "WebDAV server URL",
|
||||
@@ -717,6 +912,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - FTP
|
||||
"ftp_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable FTP as an upload destination. When disabled, no documents will be sent to FTP even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"ftp_host": {
|
||||
"category": "Storage Providers",
|
||||
"description": "FTP server hostname or IP address",
|
||||
@@ -774,6 +977,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - SFTP
|
||||
"sftp_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable SFTP as an upload destination. When disabled, no documents will be sent to SFTP even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"sftp_host": {
|
||||
"category": "Storage Providers",
|
||||
"description": "SFTP server hostname or IP address",
|
||||
@@ -838,7 +1049,56 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - iCloud Drive
|
||||
"icloud_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable iCloud Drive as an upload destination. When disabled, no documents will be sent to iCloud Drive even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"icloud_username": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Apple ID email address for iCloud Drive authentication",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"icloud_password": {
|
||||
"category": "Storage Providers",
|
||||
"description": "App-specific password for iCloud Drive (generate at https://appleid.apple.com)",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"icloud_folder": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Target folder path in iCloud Drive (e.g. Documents/Uploads)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"icloud_cookie_directory": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Directory for persisting iCloud session cookies (default: ~/.pyicloud)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - AWS S3
|
||||
"s3_enabled": {
|
||||
"category": "Storage Providers",
|
||||
"description": "Enable Amazon S3 as an upload destination. When disabled, no documents will be sent to S3 even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"aws_access_key_id": {
|
||||
"category": "Storage Providers",
|
||||
"description": "AWS access key ID for S3",
|
||||
@@ -972,6 +1232,14 @@ SETTING_METADATA = {
|
||||
"restart_required": False,
|
||||
},
|
||||
# Email Destination Settings (dedicated SMTP for document delivery)
|
||||
"dest_email_enabled": {
|
||||
"category": "Email Destination",
|
||||
"description": "Enable Email as an upload destination. When disabled, no documents will be delivered via email even if credentials are configured.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_host": {
|
||||
"category": "Email Destination",
|
||||
"description": "SMTP server hostname for document delivery (separate from shared email settings)",
|
||||
@@ -1392,6 +1660,18 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"imap_attachment_filter": {
|
||||
"category": "IMAP",
|
||||
"description": (
|
||||
"Controls which attachment types are ingested from IMAP emails. "
|
||||
"Accepted values: 'documents_only' (PDFs and office files only, default) or 'all' (including images). "
|
||||
"Per-user IMAP accounts can override this global default."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Monitoring - Uptime Kuma
|
||||
"uptime_kuma_url": {
|
||||
"category": "Monitoring",
|
||||
@@ -1595,6 +1875,18 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"compliance_enabled": {
|
||||
"category": "Feature Flags",
|
||||
"description": (
|
||||
"Enable the compliance templates dashboard (GDPR, HIPAA, SOC 2). "
|
||||
"When enabled, admins can view compliance status and apply "
|
||||
"pre-built regulatory configurations. Default: True."
|
||||
),
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Backup / Restore
|
||||
"backup_enabled": {
|
||||
"category": "Backup",
|
||||
@@ -2065,6 +2357,78 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"audit_siem_enabled": {
|
||||
"category": "Security",
|
||||
"description": "Enable forwarding of audit events to an external SIEM system (Syslog, Splunk, Logstash, etc.).",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"audit_siem_transport": {
|
||||
"category": "Security",
|
||||
"description": (
|
||||
"Transport used to forward audit events. 'syslog' sends RFC 5424 messages over UDP/TCP. "
|
||||
"'http' sends JSON POST payloads to a webhook URL (Splunk HEC, Logstash, Grafana Loki, etc.)."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
"options": ["syslog", "http"],
|
||||
},
|
||||
"audit_siem_syslog_host": {
|
||||
"category": "Security",
|
||||
"description": "Hostname or IP of the syslog receiver.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"audit_siem_syslog_port": {
|
||||
"category": "Security",
|
||||
"description": "Port of the syslog receiver. Default: 514.",
|
||||
"type": "integer",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"audit_siem_syslog_protocol": {
|
||||
"category": "Security",
|
||||
"description": "Protocol for syslog transport: 'udp' or 'tcp'. Default: udp.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
"options": ["udp", "tcp"],
|
||||
},
|
||||
"audit_siem_http_url": {
|
||||
"category": "Security",
|
||||
"description": (
|
||||
"HTTP endpoint URL for SIEM webhook delivery. Supports Splunk HEC, "
|
||||
"Logstash HTTP input, Grafana Loki push API, or any JSON-accepting endpoint."
|
||||
),
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"audit_siem_http_token": {
|
||||
"category": "Security",
|
||||
"description": "Bearer / HEC token included in the Authorization header of SIEM HTTP requests.",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"audit_siem_http_custom_headers": {
|
||||
"category": "Security",
|
||||
"description": "Comma-separated 'Key:Value' pairs of extra headers for SIEM HTTP requests.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Rate Limiting
|
||||
"rate_limiting_enabled": {
|
||||
"category": "Security",
|
||||
|
||||
@@ -210,6 +210,19 @@ def dispatch_user_notification(
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 3. Send push notifications to registered mobile devices
|
||||
try:
|
||||
from app.utils.push_notification import send_push_to_owner
|
||||
|
||||
send_push_to_owner(
|
||||
owner_id=owner_id,
|
||||
title=title,
|
||||
body=message,
|
||||
data={"event_type": event_type, "file_id": file_id},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error sending push notification for owner_id=%s event=%s", owner_id, event_type)
|
||||
|
||||
|
||||
def notify_user_document_processed(owner_id: str, filename: str, file_id: int | None = None) -> None:
|
||||
"""Notify a user that their document was successfully processed."""
|
||||
|
||||
@@ -6,7 +6,9 @@ from fastapi import APIRouter
|
||||
|
||||
from app.views.admin_users import router as admin_users_router
|
||||
from app.views.api_tokens import router as api_tokens_router
|
||||
from app.views.audit_logs import router as audit_logs_router
|
||||
from app.views.backup import router as backup_router
|
||||
from app.views.compliance import router as compliance_router
|
||||
from app.views.db_wizard import router as db_wizard_router
|
||||
from app.views.dropbox import router as dropbox_router
|
||||
from app.views.filemanager import router as filemanager_router
|
||||
@@ -23,6 +25,7 @@ from app.views.onboarding import router as onboarding_router
|
||||
from app.views.onedrive import router as onedrive_router
|
||||
from app.views.pipelines import router as pipelines_router # Processing pipelines
|
||||
from app.views.plans import router as plans_router # Admin Plan Designer
|
||||
from app.views.profile import router as profile_router # User self-service profile
|
||||
from app.views.queue import router as queue_router
|
||||
from app.views.scheduled_jobs import router as scheduled_jobs_router # Scheduled batch jobs
|
||||
from app.views.search import router as search_router
|
||||
@@ -56,8 +59,11 @@ router.include_router(subscriptions_router) # Pricing + subscription pages
|
||||
router.include_router(plans_router) # Admin Plan Designer
|
||||
router.include_router(onboarding_router) # User onboarding wizard
|
||||
router.include_router(pipelines_router) # Processing pipelines
|
||||
router.include_router(profile_router) # User self-service profile settings
|
||||
router.include_router(imap_accounts_router) # Per-user IMAP ingestion accounts
|
||||
router.include_router(integrations_router) # Unified integrations dashboard
|
||||
router.include_router(notifications_router) # User notification dashboard
|
||||
router.include_router(scheduled_jobs_router) # Admin scheduled batch jobs
|
||||
router.include_router(audit_logs_router) # Comprehensive audit log viewer
|
||||
router.include_router(help_router) # Built-in help / How-To docs
|
||||
router.include_router(compliance_router) # Compliance templates dashboard
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Audit log viewer UI — admin-only page with filtering and SIEM status.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.views.base import APIRouter, get_db, require_login, settings, templates
|
||||
from app.views.settings import require_admin_access
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/admin/audit-logs")
|
||||
@require_login
|
||||
@require_admin_access
|
||||
async def audit_logs_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Comprehensive audit log viewer with filtering controls.
|
||||
|
||||
Displays a chronological log of all significant actions: logins,
|
||||
document operations, settings changes, and admin actions. The
|
||||
actual data is fetched client-side via the ``/api/audit-logs`` JSON
|
||||
endpoint so that filters, pagination, and live refresh work without
|
||||
full-page reloads.
|
||||
"""
|
||||
try:
|
||||
siem_enabled = settings.audit_siem_enabled
|
||||
siem_transport = settings.audit_siem_transport if siem_enabled else None
|
||||
return templates.TemplateResponse(
|
||||
"audit_logs.html",
|
||||
{
|
||||
"request": request,
|
||||
"app_version": settings.version,
|
||||
"siem_enabled": siem_enabled,
|
||||
"siem_transport": siem_transport,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error loading audit logs page: %s", e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to load audit logs page",
|
||||
)
|
||||
@@ -12,6 +12,14 @@ from sqlalchemy.orm import Session # noqa: F401
|
||||
from app.auth import require_login # noqa: F401
|
||||
from app.config import settings
|
||||
from app.database import get_db # noqa: F401
|
||||
from app.utils.i18n import (
|
||||
SUPPORTED_LANGUAGES,
|
||||
detect_language,
|
||||
format_date,
|
||||
format_datetime,
|
||||
format_number,
|
||||
translate,
|
||||
)
|
||||
|
||||
# Set up Jinja2 templates
|
||||
templates_dir = Path(__file__).parent.parent.parent / "frontend" / "templates"
|
||||
@@ -21,6 +29,19 @@ templates = Jinja2Templates(directory=str(templates_dir))
|
||||
templates.env.globals["min"] = min
|
||||
templates.env.globals["max"] = max
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# i18n Jinja2 integration
|
||||
# ---------------------------------------------------------------------------
|
||||
# The _() function is available in every template to translate UI strings.
|
||||
# Usage: {{ _("nav.dashboard") }} or {{ _("upload.max_size", size="10 MB") }}
|
||||
# The locale is automatically resolved from the request context.
|
||||
# A default English implementation is registered as a global so error handlers
|
||||
# that don't go through _inject_global_context still have the function available.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
templates.env.globals["supported_languages"] = SUPPORTED_LANGUAGES
|
||||
templates.env.globals["_"] = lambda key, **kwargs: translate(key, "en", **kwargs)
|
||||
|
||||
# Customize Jinja2Templates to include app_version in all templates
|
||||
original_template_response = templates.TemplateResponse
|
||||
|
||||
@@ -48,8 +69,31 @@ def _inject_global_context(ctx: dict) -> None:
|
||||
session_user = req.session.get("user")
|
||||
# When auth is disabled every visitor is effectively "logged in"
|
||||
ctx.setdefault("is_logged_in", not getattr(settings, "auth_enabled", True) or session_user is not None)
|
||||
|
||||
# --- i18n: detect language and register template helpers ---
|
||||
current_locale = detect_language(req)
|
||||
ctx.setdefault("current_locale", current_locale)
|
||||
|
||||
def _translate(key: str, **kwargs: object) -> str:
|
||||
return translate(key, current_locale, **kwargs)
|
||||
|
||||
def _format_date(value: object, short: bool = False) -> str:
|
||||
return format_date(value, current_locale, short=short) # type: ignore[arg-type]
|
||||
|
||||
def _format_datetime(value: object) -> str:
|
||||
return format_datetime(value, current_locale) # type: ignore[arg-type]
|
||||
|
||||
def _format_number(value: object) -> str:
|
||||
return format_number(value, current_locale) # type: ignore[arg-type]
|
||||
|
||||
ctx.setdefault("_", _translate)
|
||||
ctx.setdefault("format_date_l10n", _format_date)
|
||||
ctx.setdefault("format_datetime_l10n", _format_datetime)
|
||||
ctx.setdefault("format_number_l10n", _format_number)
|
||||
else:
|
||||
ctx.setdefault("is_logged_in", not getattr(settings, "auth_enabled", True))
|
||||
ctx.setdefault("current_locale", "en")
|
||||
ctx.setdefault("_", lambda key, **kw: translate(key, "en", **kw))
|
||||
|
||||
|
||||
def template_response_with_version(*args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Admin view: compliance templates dashboard page."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.views.base import APIRouter, require_login, settings, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _require_admin(request: Request):
|
||||
"""Return the session user if they are an admin, else None."""
|
||||
user = request.session.get("user")
|
||||
if not user or not user.get("is_admin"):
|
||||
logger.warning("Non-admin user attempted to access /admin/compliance")
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/admin/compliance")
|
||||
@require_login
|
||||
async def compliance_page(request: Request):
|
||||
"""Admin compliance templates dashboard page.
|
||||
|
||||
Displays GDPR, HIPAA, and SOC2 compliance templates with their current
|
||||
status and one-click apply functionality.
|
||||
"""
|
||||
user = _require_admin(request)
|
||||
if user is None:
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
try:
|
||||
return templates.TemplateResponse(
|
||||
"compliance.html",
|
||||
{
|
||||
"request": request,
|
||||
"app_version": settings.version,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading compliance page: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to load compliance page",
|
||||
)
|
||||
@@ -1,11 +1,13 @@
|
||||
"""User-facing view for the per-user IMAP ingestion dashboard."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import UserImapAccount
|
||||
from app.models import ImapIngestionProfile, UserImapAccount
|
||||
from app.utils.allowed_types import DEFAULT_CATEGORIES, FILE_TYPE_CATEGORIES
|
||||
from app.utils.subscription import get_tier, get_user_tier_id
|
||||
from app.utils.user_scope import get_current_owner_id
|
||||
from app.views.base import APIRouter, Depends, get_db, require_login, templates
|
||||
@@ -25,6 +27,22 @@ def _get_max_mailboxes(tier: dict) -> int | None:
|
||||
return max_mb
|
||||
|
||||
|
||||
def _serialize_profile(profile: ImapIngestionProfile) -> dict:
|
||||
"""Serialize a profile for JSON embedding in the template."""
|
||||
try:
|
||||
categories = json.loads(profile.allowed_categories)
|
||||
except (ValueError, TypeError):
|
||||
categories = []
|
||||
return {
|
||||
"id": profile.id,
|
||||
"name": profile.name,
|
||||
"description": profile.description,
|
||||
"owner_id": profile.owner_id,
|
||||
"allowed_categories": categories,
|
||||
"is_builtin": profile.is_builtin,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/imap-accounts")
|
||||
@require_login
|
||||
async def imap_accounts_page(request: Request, db: Session = Depends(get_db)):
|
||||
@@ -49,11 +67,35 @@ async def imap_accounts_page(request: Request, db: Session = Depends(get_db)):
|
||||
max_mailboxes = _get_max_mailboxes(tier)
|
||||
can_add = max_mailboxes is None or (max_mailboxes > 0 and current_count < max_mailboxes)
|
||||
|
||||
# Load ingestion profiles: system-global + user's own
|
||||
profiles = (
|
||||
db.query(ImapIngestionProfile)
|
||||
.filter(
|
||||
# SQLAlchemy requires `== None` for IS NULL comparison in ORM filters
|
||||
(ImapIngestionProfile.owner_id == None) | (ImapIngestionProfile.owner_id == owner_id) # noqa: E711
|
||||
)
|
||||
.order_by(ImapIngestionProfile.is_builtin.desc(), ImapIngestionProfile.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Category definitions for the UI checkbox builder
|
||||
categories = [
|
||||
{
|
||||
"key": key,
|
||||
"label": info["label"],
|
||||
"description": info["description"],
|
||||
}
|
||||
for key, info in FILE_TYPE_CATEGORIES.items()
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"imap_accounts.html",
|
||||
{
|
||||
"request": request,
|
||||
"accounts": accounts,
|
||||
"profiles": [_serialize_profile(p) for p in profiles],
|
||||
"categories": categories,
|
||||
"default_categories": DEFAULT_CATEGORIES,
|
||||
"current_count": current_count,
|
||||
"max_mailboxes": max_mailboxes,
|
||||
"can_add": can_add,
|
||||
|
||||
@@ -27,6 +27,7 @@ _DESTINATION_META: list[dict] = [
|
||||
{"id": "webdav", "name": "WebDAV", "icon": "fas fa-server"},
|
||||
{"id": "sftp", "name": "SFTP", "icon": "fas fa-terminal"},
|
||||
{"id": "ftp", "name": "FTP", "icon": "fas fa-server"},
|
||||
{"id": "icloud", "name": "iCloud Drive", "icon": "fab fa-apple"},
|
||||
]
|
||||
|
||||
|
||||
@@ -51,6 +52,7 @@ def _get_configured_destinations(cfg: Settings) -> list[dict]:
|
||||
"webdav": bool(cfg.webdav_url and cfg.webdav_username),
|
||||
"sftp": bool(cfg.sftp_host and cfg.sftp_username),
|
||||
"ftp": bool(cfg.ftp_host and cfg.ftp_username),
|
||||
"icloud": bool(cfg.icloud_username and cfg.icloud_password),
|
||||
}
|
||||
return [meta for meta in _DESTINATION_META if checks.get(meta["id"], False)]
|
||||
|
||||
|
||||
+1
-2
@@ -3,12 +3,11 @@
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app.auth import require_login
|
||||
from app.views.base import templates
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="frontend/templates")
|
||||
|
||||
|
||||
@router.get("/admin/plans", response_class=HTMLResponse)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""View route for the user self-service profile settings page.
|
||||
|
||||
Route:
|
||||
GET /profile — renders the profile settings HTML page (requires login)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import UserProfile
|
||||
from app.utils.i18n import SUPPORTED_LANGUAGES
|
||||
from app.views.base import APIRouter, get_db, require_login, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile", include_in_schema=False)
|
||||
@require_login
|
||||
async def profile_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Serve the user profile settings page."""
|
||||
user = request.session.get("user") or {}
|
||||
user_id = user.get("sub") or user.get("preferred_username") or user.get("email") or user.get("id")
|
||||
|
||||
profile = None
|
||||
if user_id:
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"profile.html",
|
||||
{
|
||||
"request": request,
|
||||
"profile": profile,
|
||||
"supported_languages": SUPPORTED_LANGUAGES,
|
||||
},
|
||||
)
|
||||
+188
@@ -2217,3 +2217,191 @@ print(response.json())
|
||||
## Further Assistance
|
||||
|
||||
For additional help with the API, please contact our support team or refer to the [Development Guide](../CONTRIBUTING.md).
|
||||
|
||||
## Mobile App API
|
||||
|
||||
The mobile API provides endpoints used by the native iOS and Android app. All endpoints require authentication (Bearer token or active session cookie).
|
||||
|
||||
For full mobile app documentation see [MobileApp.md](./MobileApp.md).
|
||||
|
||||
### POST /api/mobile/generate-token
|
||||
|
||||
Exchange an active web session for a long-lived API token scoped to the mobile app.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "device_name": "John's iPhone" }
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"token": "de_AbCdEfGhIjKl...",
|
||||
"token_id": 42,
|
||||
"name": "Mobile App – John's iPhone",
|
||||
"created_at": "2026-03-10T09:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
> The `token` is shown **once only**.
|
||||
|
||||
### POST /api/mobile/register-device
|
||||
|
||||
Register an Expo push token to receive push notifications.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"push_token": "ExponentPushToken[xxxxxx]",
|
||||
"device_name": "John's iPhone",
|
||||
"platform": "ios"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):** Device record with `id`, `platform`, `is_active`, `created_at`.
|
||||
|
||||
### GET /api/mobile/devices
|
||||
|
||||
List all registered push-notification devices for the current user.
|
||||
|
||||
**Response (200 OK):** Array of device records.
|
||||
|
||||
### DELETE /api/mobile/devices/{device_id}
|
||||
|
||||
Deactivate a push-notification device. The device will no longer receive push notifications.
|
||||
|
||||
**Response (204 No Content)**
|
||||
|
||||
### GET /api/mobile/whoami
|
||||
|
||||
Return basic profile information for the authenticated user.
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"owner_id": "john@example.com",
|
||||
"display_name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/...",
|
||||
"is_admin": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GraphQL API
|
||||
|
||||
DocuElevate exposes a GraphQL API at `/graphql` alongside the REST API. It
|
||||
supports flexible queries with field selection, making it ideal for dashboards
|
||||
and integrations that only need a subset of the available data.
|
||||
|
||||
### Endpoint
|
||||
|
||||
| Method | URL | Description |
|
||||
|--------|-----|-------------|
|
||||
| `POST` | `/graphql` | Execute a GraphQL query or mutation |
|
||||
| `GET` | `/graphql` | Open the GraphiQL interactive playground |
|
||||
|
||||
### Authentication
|
||||
|
||||
The GraphQL endpoint honours the same authentication rules as the REST API:
|
||||
|
||||
- **`AUTH_ENABLED=False`** (default, single-user mode): all queries are
|
||||
allowed without credentials.
|
||||
- **`AUTH_ENABLED=True`** (multi-user mode): a valid session cookie **or**
|
||||
an `Authorization: Bearer <token>` API token is required. Admin-only
|
||||
queries (settings, users) additionally require the `is_admin` flag.
|
||||
|
||||
### Available Queries
|
||||
|
||||
| Field | Returns | Notes |
|
||||
|-------|---------|-------|
|
||||
| `documents(ownerId, limit, offset)` | `[DocumentType]` | Paginated list of documents |
|
||||
| `document(id)` | `DocumentType` | Single document by primary key |
|
||||
| `pipelines(ownerId, limit, offset)` | `[PipelineType]` | Paginated list of pipelines with steps |
|
||||
| `pipeline(id)` | `PipelineType` | Single pipeline by primary key |
|
||||
| `settings(limit, offset)` | `[SettingType]` | Non-sensitive app settings (**admin only**) |
|
||||
| `users(limit, offset)` | `[UserType]` | User profiles (**admin only**) |
|
||||
| `user(userId)` | `UserType` | Single user profile (**admin only**) |
|
||||
|
||||
> **Note:** Sensitive configuration keys (API secrets, passwords, tokens) are
|
||||
> automatically excluded from the `settings` query regardless of the caller's
|
||||
> privilege level.
|
||||
|
||||
### GraphiQL Playground
|
||||
|
||||
Navigate to `http://<your-instance>/graphql` in a browser to open the
|
||||
interactive GraphiQL IDE, which provides schema documentation, auto-complete,
|
||||
and the ability to run queries directly.
|
||||
|
||||
### Example Queries
|
||||
|
||||
**List recent documents:**
|
||||
```graphql
|
||||
{
|
||||
documents(limit: 5) {
|
||||
id
|
||||
originalFilename
|
||||
mimeType
|
||||
fileSize
|
||||
documentTitle
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fetch a pipeline with its steps:**
|
||||
```graphql
|
||||
{
|
||||
pipeline(id: 1) {
|
||||
id
|
||||
name
|
||||
description
|
||||
isDefault
|
||||
isActive
|
||||
steps {
|
||||
position
|
||||
stepType
|
||||
label
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**List application settings (admin only):**
|
||||
```graphql
|
||||
{
|
||||
settings {
|
||||
key
|
||||
value
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**List user profiles (admin only):**
|
||||
```graphql
|
||||
{
|
||||
users(limit: 10) {
|
||||
userId
|
||||
displayName
|
||||
subscriptionTier
|
||||
isBlocked
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Using variables:**
|
||||
```graphql
|
||||
query GetDocument($id: Int!) {
|
||||
document(id: $id) {
|
||||
id
|
||||
originalFilename
|
||||
documentTitle
|
||||
isDuplicate
|
||||
ocrQualityScore
|
||||
}
|
||||
}
|
||||
```
|
||||
Variables: `{ "id": 42 }`
|
||||
|
||||
@@ -20,10 +20,11 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
DocuElevate supports two primary authentication methods:
|
||||
DocuElevate supports multiple authentication methods that can be used independently or together:
|
||||
|
||||
1. **Simple Authentication** - Basic username/password authentication managed by DocuElevate
|
||||
2. **OpenID Connect** - Integration with identity providers like Authentik, Keycloak, or Auth0
|
||||
3. **Social Login** - Sign in with Google, Microsoft, Apple, or Dropbox accounts (see [Social Login Setup Guide](SocialLoginSetup.md))
|
||||
|
||||
## Session Security
|
||||
|
||||
@@ -189,3 +190,11 @@ If you encounter issues with authentication:
|
||||
- For most providers, you can visit the `/.well-known/openid-configuration` endpoint to verify their settings
|
||||
|
||||
For more general configuration issues, see the [Configuration Troubleshooting Guide](ConfigurationTroubleshooting.md).
|
||||
|
||||
## Social Login
|
||||
|
||||
DocuElevate supports social login with Google, Microsoft, Apple, and Dropbox. Social login allows users to authenticate using their existing accounts with these providers, without needing a separate DocuElevate password.
|
||||
|
||||
Social login can be used alongside any other authentication method (simple auth, OIDC, local signup). Each social provider is independently configured.
|
||||
|
||||
For detailed setup instructions, prerequisites, and provider-specific configuration, see the **[Social Login Setup Guide](SocialLoginSetup.md)**.
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Compliance Templates Guide
|
||||
|
||||
DocuElevate includes pre-built compliance templates for **GDPR**, **HIPAA**, and **SOC 2** that help you configure your instance to meet regulatory requirements. This guide covers how to use the compliance dashboard, apply templates, and monitor your compliance status.
|
||||
|
||||
## Overview
|
||||
|
||||
The compliance templates feature provides:
|
||||
|
||||
- **Pre-built configurations** for GDPR, HIPAA, and SOC 2
|
||||
- **One-click apply** to configure all required settings at once
|
||||
- **Compliance status dashboard** to monitor your regulatory posture
|
||||
- **Individual check results** showing which settings are compliant and which need attention
|
||||
|
||||
## Accessing the Dashboard
|
||||
|
||||
The compliance dashboard is available to **admin users only**.
|
||||
|
||||
1. Log in as an administrator
|
||||
2. Click **Admin** in the navigation bar
|
||||
3. Select **Compliance** from the dropdown menu
|
||||
|
||||
Or navigate directly to: `/admin/compliance`
|
||||
|
||||
## Available Templates
|
||||
|
||||
### GDPR (General Data Protection Regulation)
|
||||
|
||||
The European Union regulation for data protection and privacy. The GDPR template enforces:
|
||||
|
||||
| Setting | Value | Purpose |
|
||||
|---------|-------|---------|
|
||||
| `AUTH_ENABLED` | `True` | Controls access to personal data |
|
||||
| `SENTRY_SEND_DEFAULT_PII` | `False` | Prevents PII leaking to external services |
|
||||
| `SECURITY_HEADERS_ENABLED` | `True` | Protects against common web vulnerabilities |
|
||||
| `SECURITY_HEADER_HSTS_ENABLED` | `True` | Ensures encrypted connections |
|
||||
| `SECURITY_HEADER_CSP_ENABLED` | `True` | Prevents XSS and injection attacks |
|
||||
| `SECURITY_HEADER_X_FRAME_OPTIONS_ENABLED` | `True` | Prevents clickjacking |
|
||||
| `ENABLE_DEDUPLICATION` | `True` | Data minimisation — avoids duplicate storage |
|
||||
|
||||
### HIPAA (Health Insurance Portability and Accountability Act)
|
||||
|
||||
United States regulation for protecting health information. The HIPAA template includes all GDPR settings plus:
|
||||
|
||||
| Setting | Value | Purpose |
|
||||
|---------|-------|---------|
|
||||
| `MULTI_USER_ENABLED` | `True` | Individual accounts for access accountability |
|
||||
|
||||
### SOC 2 (Service Organization Control 2)
|
||||
|
||||
Trust Service Criteria framework for service organisations. The SOC 2 template includes the same settings as HIPAA, mapped to SOC 2 Trust Service Criteria (CC6.x, PI1.x).
|
||||
|
||||
## Applying a Template
|
||||
|
||||
1. Navigate to the **Compliance** dashboard (`/admin/compliance`)
|
||||
2. Find the template you want to apply (GDPR, HIPAA, or SOC 2)
|
||||
3. Click **Apply Template**
|
||||
4. Confirm the action in the dialog
|
||||
5. The template settings are written to the database immediately
|
||||
|
||||
> **Note:** Applying a template writes configuration values to the database. Some settings (e.g., security headers) may require a restart to take effect. Check the Settings page for restart indicators.
|
||||
|
||||
## Understanding Compliance Status
|
||||
|
||||
Each template shows one of four statuses:
|
||||
|
||||
| Status | Badge | Meaning |
|
||||
|--------|-------|---------|
|
||||
| **Compliant** | Green | All checks are passing |
|
||||
| **Partial** | Yellow | Some checks are passing, others are not |
|
||||
| **Non-Compliant** | Red | No checks are passing |
|
||||
| **Not Applied** | Grey | Template has never been applied |
|
||||
|
||||
### Individual Checks
|
||||
|
||||
Click **Show Details** on any template card to see individual check results:
|
||||
|
||||
- ✅ **Passing** — The setting matches the expected compliance value
|
||||
- ❌ **Failing** — The setting does not match; the current and expected values are shown
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The compliance feature exposes the following API endpoints under `/api/compliance/`:
|
||||
|
||||
### List Templates
|
||||
|
||||
```bash
|
||||
GET /api/compliance/templates
|
||||
```
|
||||
|
||||
Returns all compliance templates with their current status.
|
||||
|
||||
### Get Single Template
|
||||
|
||||
```bash
|
||||
GET /api/compliance/templates/{name}
|
||||
```
|
||||
|
||||
Returns a single template by name (`gdpr`, `hipaa`, or `soc2`).
|
||||
|
||||
### Apply Template
|
||||
|
||||
```bash
|
||||
POST /api/compliance/templates/{name}/apply
|
||||
```
|
||||
|
||||
Applies a compliance template, writing all its settings to the database.
|
||||
|
||||
### Get Template Status
|
||||
|
||||
```bash
|
||||
GET /api/compliance/templates/{name}/status
|
||||
```
|
||||
|
||||
Evaluates the live compliance status of a template against current settings.
|
||||
|
||||
**Response example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "partial",
|
||||
"total": 7,
|
||||
"passed": 5,
|
||||
"failed": 2,
|
||||
"check_results": [
|
||||
{
|
||||
"key": "auth_enabled",
|
||||
"label": "Authentication enabled",
|
||||
"description": "User authentication must be enabled to control access to personal data.",
|
||||
"expected": "True",
|
||||
"actual": "True",
|
||||
"passing": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compliance Summary
|
||||
|
||||
```bash
|
||||
GET /api/compliance/summary
|
||||
```
|
||||
|
||||
Returns an overall compliance summary across all templates.
|
||||
|
||||
**Response example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"overall_status": "partial",
|
||||
"total_checks": 22,
|
||||
"total_passed": 18,
|
||||
"total_failed": 4,
|
||||
"templates": [
|
||||
{
|
||||
"name": "gdpr",
|
||||
"display_name": "GDPR (General Data Protection Regulation)",
|
||||
"enabled": true,
|
||||
"status": "compliant",
|
||||
"total": 7,
|
||||
"passed": 7,
|
||||
"failed": 0,
|
||||
"applied_at": "2026-03-09T12:00:00+00:00",
|
||||
"applied_by": "admin@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** All API endpoints require admin authentication.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `COMPLIANCE_ENABLED` | `True` | Enable the compliance templates dashboard. Set to `False` to hide the feature. |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Apply templates before going live** — Set up compliance before processing real documents
|
||||
2. **Monitor status regularly** — Check the compliance dashboard after configuration changes
|
||||
3. **Use the refresh button** — After changing settings elsewhere, refresh the compliance page to see updated status
|
||||
4. **Combine templates** — You can apply multiple templates; settings overlap is handled automatically
|
||||
5. **Review after updates** — After upgrading DocuElevate, review your compliance status as new checks may be added
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Configuration Guide](./ConfigurationGuide.md) — Full list of configuration options
|
||||
- [Privacy & Compliance Guide](./PrivacyCompliance.md) — Privacy notice and GDPR compliance details
|
||||
- [Deployment Guide](./DeploymentGuide.md) — Production deployment with security best practices
|
||||
- [Security Audit](../SECURITY_AUDIT.md) — Security findings and mitigations
|
||||
@@ -16,6 +16,7 @@ Configuration is primarily done through environment variables specified in a `.e
|
||||
| `GOTENBERG_URL` | Gotenberg PDF processing URL. | `http://gotenberg:3000` |
|
||||
| `EXTERNAL_HOSTNAME` | The external hostname for the application. | `docuelevate.example.com` |
|
||||
| `ALLOW_FILE_DELETE` | Enable file deletion in the web interface (`true`/`false`). | `true` |
|
||||
| `COMPLIANCE_ENABLED` | Enable the compliance templates dashboard (GDPR, HIPAA, SOC 2). | `true` |
|
||||
|
||||
### Batch Processing Settings
|
||||
|
||||
@@ -303,6 +304,42 @@ DocuElevate can automatically pull document attachments from IMAP mailboxes —
|
||||
| `IMAP1_SSL` | Use SSL (`true`/`false`). | `true` |
|
||||
| `IMAP1_POLL_INTERVAL_MINUTES` | Frequency in minutes to poll for new mail. | `5` |
|
||||
| `IMAP_READONLY_MODE` | When `true`, fetches and processes attachments but does **not** modify the mailbox (no starring, labeling, deleting, or flag changes). Use for pre-production instances sharing a mailbox with production. Default: `false`. | `false` |
|
||||
| `IMAP_ATTACHMENT_FILTER` | System-wide fallback for which attachment types are ingested when no ingestion profile is assigned to a mailbox. `documents_only` (default) ingests PDFs and office files only — images are skipped. `all` ingests every supported file type including images. Individual IMAP accounts can override this using ingestion profiles. | `documents_only` |
|
||||
|
||||
#### IMAP Ingestion Profiles
|
||||
|
||||
For fine-grained control, DocuElevate supports **Ingestion Profiles** — named configurations that let you choose exactly which file-type categories to accept from each mailbox.
|
||||
|
||||
Each profile contains a list of enabled **categories**:
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| `pdf` | PDF documents (`.pdf`) |
|
||||
| `office` | Microsoft Office files (Word, Excel, PowerPoint — `.docx`, `.xlsx`, `.pptx`, …) |
|
||||
| `opendocument` | LibreOffice/OpenOffice files (`.odt`, `.ods`, `.odp`, …) |
|
||||
| `text` | Plain text, CSV and RTF files (`.txt`, `.csv`, `.rtf`) |
|
||||
| `web` | HTML and Markdown files (`.html`, `.htm`, `.md`, `.markdown`) |
|
||||
| `images` | Image files (`.jpg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg`) |
|
||||
|
||||
Two built-in system profiles are seeded automatically:
|
||||
|
||||
| Profile | Categories |
|
||||
|---------|------------|
|
||||
| **Documents Only** | pdf, office, opendocument, text, web (no images) |
|
||||
| **All Files** | All categories, including images |
|
||||
|
||||
Users can create their own custom profiles via the **Email Ingestion** dashboard (`/imap-accounts`) by clicking the **Manage profiles** link or the **+** button next to the profile dropdown. Custom profiles are private to the creating user and can be freely edited or deleted.
|
||||
|
||||
**API endpoints for ingestion profiles:**
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/imap-profiles/` | List all visible profiles (system + user's own) |
|
||||
| `POST` | `/api/imap-profiles/` | Create a new profile |
|
||||
| `GET` | `/api/imap-profiles/categories` | List available file-type categories |
|
||||
| `GET` | `/api/imap-profiles/{id}` | Get a single profile |
|
||||
| `PUT` | `/api/imap-profiles/{id}` | Update a profile (not built-in) |
|
||||
| `DELETE` | `/api/imap-profiles/{id}` | Delete a profile (not built-in) |
|
||||
|
||||
#### Per-User IMAP Integrations
|
||||
|
||||
@@ -335,6 +372,28 @@ Credentials are encrypted at rest using Fernet encryption.
|
||||
| `AUTHENTIK_CONFIG_URL` | Configuration URL for Authentik OpenID Connect. |
|
||||
| `OAUTH_PROVIDER_NAME` | Display name for the OAuth provider button. |
|
||||
|
||||
### Social Login Providers
|
||||
|
||||
Social login lets users sign in with their existing Google, Microsoft, Apple, or Dropbox accounts. Each provider is independently enabled and configured. For detailed setup instructions see the [Social Login Setup Guide](SocialLoginSetup.md).
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|---|---|---|
|
||||
| `SOCIAL_AUTH_GOOGLE_ENABLED` | Enable Google Sign-In. | `false` |
|
||||
| `SOCIAL_AUTH_GOOGLE_CLIENT_ID` | Google OAuth2 client ID from the Google Cloud Console. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GOOGLE_CLIENT_SECRET` | Google OAuth2 client secret. | *(empty)* |
|
||||
| `SOCIAL_AUTH_MICROSOFT_ENABLED` | Enable Microsoft Sign-In (Azure AD / Microsoft Entra ID). | `false` |
|
||||
| `SOCIAL_AUTH_MICROSOFT_CLIENT_ID` | Microsoft application (client) ID from Azure App Registrations. | *(empty)* |
|
||||
| `SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET` | Microsoft client secret. | *(empty)* |
|
||||
| `SOCIAL_AUTH_MICROSOFT_TENANT` | Azure AD tenant: `common`, `organizations`, `consumers`, or a tenant GUID. | `common` |
|
||||
| `SOCIAL_AUTH_APPLE_ENABLED` | Enable Sign in with Apple. | `false` |
|
||||
| `SOCIAL_AUTH_APPLE_CLIENT_ID` | Apple Services ID (e.g. `com.example.docuelevate`). | *(empty)* |
|
||||
| `SOCIAL_AUTH_APPLE_TEAM_ID` | Apple Developer Team ID. | *(empty)* |
|
||||
| `SOCIAL_AUTH_APPLE_KEY_ID` | Apple Sign-In private key ID. | *(empty)* |
|
||||
| `SOCIAL_AUTH_APPLE_PRIVATE_KEY` | Apple Sign-In private key (PEM format). | *(empty)* |
|
||||
| `SOCIAL_AUTH_DROPBOX_ENABLED` | Enable Dropbox Sign-In. | `false` |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_ID` | Dropbox OAuth2 App Key. | *(empty)* |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_SECRET` | Dropbox OAuth2 App Secret. | *(empty)* |
|
||||
|
||||
### Multi-User Mode
|
||||
|
||||
When multi-user mode is enabled, each authenticated user gets their own isolated document space.
|
||||
@@ -398,6 +457,61 @@ default overage buffer applied across all plans.
|
||||
|
||||
DocuElevate supports HTTP security headers to improve browser-side security. **These headers are disabled by default** since most deployments use a reverse proxy (Traefik, Nginx, etc.) that already adds them. Enable only if deploying directly without a reverse proxy. See [Deployment Guide - Security Headers](DeploymentGuide.md#security-headers) for detailed configuration examples.
|
||||
|
||||
### Audit Logging
|
||||
|
||||
DocuElevate provides comprehensive audit logging that records significant actions (logins, document CRUD, settings changes) to an append-only database table. Every entry captures the timestamp, user, action, resource, client IP, and optional JSON details.
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|--------------------------------|---------------------------------------------------------------------------------------------------|-------------|
|
||||
| `AUDIT_LOGGING_ENABLED` | Enable the HTTP request audit-logging middleware. | `true` |
|
||||
| `AUDIT_LOG_INCLUDE_CLIENT_IP` | Include the client IP address in audit log entries. Disable for GDPR-sensitive deployments. | `true` |
|
||||
|
||||
#### SIEM Integration
|
||||
|
||||
Audit events can be forwarded in real time to external SIEM systems for centralised monitoring, alerting, and long-term retention. Two transports are supported:
|
||||
|
||||
* **Syslog** – RFC 5424 structured-data messages over UDP or TCP. Works with rsyslog, syslog-ng, Graylog, Datadog, etc.
|
||||
* **HTTP** – JSON POST payloads compatible with Splunk HEC, Logstash HTTP input, Grafana Loki push API, and any generic webhook.
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|-------------------------------------|---------------------------------------------------------------------------------------------------|---------------|
|
||||
| `AUDIT_SIEM_ENABLED` | Enable forwarding of audit events to an external SIEM system. | `false` |
|
||||
| `AUDIT_SIEM_TRANSPORT` | Transport: `syslog` or `http`. | `syslog` |
|
||||
| `AUDIT_SIEM_SYSLOG_HOST` | Hostname or IP of the syslog receiver. | `localhost` |
|
||||
| `AUDIT_SIEM_SYSLOG_PORT` | Port of the syslog receiver. | `514` |
|
||||
| `AUDIT_SIEM_SYSLOG_PROTOCOL` | Protocol for syslog: `udp` or `tcp`. | `udp` |
|
||||
| `AUDIT_SIEM_HTTP_URL` | HTTP endpoint URL for SIEM delivery (e.g. Splunk HEC, Logstash, Loki). | *(empty)* |
|
||||
| `AUDIT_SIEM_HTTP_TOKEN` | Bearer / HEC token for the SIEM HTTP endpoint. | *(empty)* |
|
||||
| `AUDIT_SIEM_HTTP_CUSTOM_HEADERS` | Comma-separated `Key:Value` extra headers for SIEM HTTP requests. | *(empty)* |
|
||||
|
||||
**Example – Syslog to rsyslog:**
|
||||
|
||||
```bash
|
||||
AUDIT_SIEM_ENABLED=true
|
||||
AUDIT_SIEM_TRANSPORT=syslog
|
||||
AUDIT_SIEM_SYSLOG_HOST=syslog.internal.example.com
|
||||
AUDIT_SIEM_SYSLOG_PORT=514
|
||||
AUDIT_SIEM_SYSLOG_PROTOCOL=udp
|
||||
```
|
||||
|
||||
**Example – Splunk HEC:**
|
||||
|
||||
```bash
|
||||
AUDIT_SIEM_ENABLED=true
|
||||
AUDIT_SIEM_TRANSPORT=http
|
||||
AUDIT_SIEM_HTTP_URL=https://splunk.example.com:8088/services/collector/event
|
||||
AUDIT_SIEM_HTTP_TOKEN=your-hec-token
|
||||
```
|
||||
|
||||
**Example – Logstash HTTP input:**
|
||||
|
||||
```bash
|
||||
AUDIT_SIEM_ENABLED=true
|
||||
AUDIT_SIEM_TRANSPORT=http
|
||||
AUDIT_SIEM_HTTP_URL=https://logstash.example.com:8080
|
||||
AUDIT_SIEM_HTTP_TOKEN=
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
DocuElevate implements rate limiting to protect against DoS attacks and API abuse. **Rate limiting is enabled by default** and uses Redis for distributed rate limiting across multiple workers.
|
||||
@@ -887,6 +1001,7 @@ TESSERACT_LANGUAGE=eng+deu
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
| `PAPERLESS_ENABLED` | Set to `false` to disable Paperless-ngx uploads without removing credentials. Default: `true` |
|
||||
| `PAPERLESS_NGX_API_TOKEN` | API token for Paperless NGX. |
|
||||
| `PAPERLESS_HOST` | Root URL for Paperless NGX (e.g. `https://paperless.example.com`). |
|
||||
| `PAPERLESS_CUSTOM_FIELD_ABSENDER` | (Optional, Legacy) Name of the custom field in Paperless-ngx to store the sender ("absender") information. If set, the extracted sender will be automatically set as a custom field after document upload. Example: `Absender` or `Sender` |
|
||||
@@ -929,6 +1044,7 @@ PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|--------------------------------------------------|
|
||||
| `DROPBOX_ENABLED` | Set to `false` to disable Dropbox uploads without removing credentials. Default: `true` |
|
||||
| `DROPBOX_APP_KEY` | Dropbox API app key. |
|
||||
| `DROPBOX_APP_SECRET` | Dropbox API app secret. |
|
||||
| `DROPBOX_REFRESH_TOKEN` | OAuth2 refresh token for Dropbox. |
|
||||
@@ -940,6 +1056,7 @@ For detailed setup instructions, see the [Dropbox Setup Guide](DropboxSetup.md).
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|---------------------------------------------------------------|
|
||||
| `NEXTCLOUD_ENABLED` | Set to `false` to disable Nextcloud uploads without removing credentials. Default: `true` |
|
||||
| `NEXTCLOUD_UPLOAD_URL` | Nextcloud WebDAV URL (e.g. `https://nc.example.com/remote.php/dav/files/<USERNAME>`). |
|
||||
| `NEXTCLOUD_USERNAME` | Nextcloud login username. |
|
||||
| `NEXTCLOUD_PASSWORD` | Nextcloud login password. |
|
||||
@@ -949,6 +1066,7 @@ For detailed setup instructions, see the [Dropbox Setup Guide](DropboxSetup.md).
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `GOOGLE_DRIVE_ENABLED` | Set to `false` to disable Google Drive uploads without removing credentials. Default: `true` |
|
||||
| `GOOGLE_DRIVE_USE_OAUTH` | Set to `true` to use OAuth flow (recommended) |
|
||||
| `GOOGLE_DRIVE_CLIENT_ID` | OAuth Client ID (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_CLIENT_SECRET` | OAuth Client Secret (required if using OAuth flow) |
|
||||
@@ -965,6 +1083,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|---------------------------------------------------------------|
|
||||
| `WEBDAV_ENABLED` | Set to `false` to disable WebDAV uploads without removing credentials. Default: `true` |
|
||||
| `WEBDAV_URL` | WebDAV server URL (e.g. `https://webdav.example.com/path`). |
|
||||
| `WEBDAV_USERNAME` | WebDAV authentication username. |
|
||||
| `WEBDAV_PASSWORD` | WebDAV authentication password. |
|
||||
@@ -975,6 +1094,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|---------------------------------------------------------------|
|
||||
| `FTP_ENABLED` | Set to `false` to disable FTP uploads without removing credentials. Default: `true` |
|
||||
| `FTP_HOST` | FTP server hostname or IP address. |
|
||||
| `FTP_PORT` | FTP port (default: `21`). |
|
||||
| `FTP_USERNAME` | FTP authentication username. |
|
||||
@@ -987,6 +1107,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|------------------------------|-------------------------------------------------------|
|
||||
| `SFTP_ENABLED` | Set to `false` to disable SFTP uploads without removing credentials. Default: `true` |
|
||||
| `SFTP_HOST` | SFTP server hostname or IP address. |
|
||||
| `SFTP_PORT` | SFTP port (default: `22`). |
|
||||
| `SFTP_USERNAME` | SFTP authentication username. |
|
||||
@@ -1018,6 +1139,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|----------------------------------|---------------------------------------------------------------------|
|
||||
| `DEST_EMAIL_ENABLED` | Set to `false` to disable email delivery without removing credentials. Default: `true` |
|
||||
| `DEST_EMAIL_HOST` | SMTP server hostname for document delivery. |
|
||||
| `DEST_EMAIL_PORT` | SMTP port for document delivery (default: `587`). |
|
||||
| `DEST_EMAIL_USERNAME` | SMTP authentication username for document delivery. |
|
||||
@@ -1030,6 +1152,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `ONEDRIVE_ENABLED` | Set to `false` to disable OneDrive uploads without removing credentials. Default: `true` |
|
||||
| `ONEDRIVE_CLIENT_ID` | Azure AD application client ID |
|
||||
| `ONEDRIVE_CLIENT_SECRET` | Azure AD application client secret |
|
||||
| `ONEDRIVE_TENANT_ID` | Azure AD tenant ID: use "common" for personal accounts or your tenant ID for corporate accounts |
|
||||
@@ -1042,6 +1165,7 @@ For detailed setup instructions, see the [OneDrive Setup Guide](OneDriveSetup.md
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `S3_ENABLED` | Set to `false` to disable S3 uploads without removing credentials. Default: `true` |
|
||||
| `AWS_ACCESS_KEY_ID` | AWS IAM access key ID |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS IAM secret access key |
|
||||
| `AWS_REGION` | AWS region where your S3 bucket is located (default: `us-east-1`) |
|
||||
@@ -1052,6 +1176,23 @@ For detailed setup instructions, see the [OneDrive Setup Guide](OneDriveSetup.md
|
||||
|
||||
For detailed setup instructions, see the [Amazon S3 Setup Guide](AmazonS3Setup.md).
|
||||
|
||||
### iCloud Drive (Apple)
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `ICLOUD_ENABLED` | Set to `false` to disable iCloud uploads without removing credentials. Default: `true` |
|
||||
| `ICLOUD_USERNAME` | Apple ID email address |
|
||||
| `ICLOUD_PASSWORD` | App-specific password (generate at [appleid.apple.com](https://appleid.apple.com/account/manage)) |
|
||||
| `ICLOUD_FOLDER` | Target folder path in iCloud Drive (e.g. `Documents/Uploads`) |
|
||||
| `ICLOUD_COOKIE_DIRECTORY` | Optional directory for session cookie persistence (default: `~/.pyicloud`) |
|
||||
|
||||
> **Note:** Apple does not provide a public REST API for iCloud Drive. This
|
||||
> integration uses the [pyicloud](https://github.com/picklepete/pyicloud)
|
||||
> library which relies on an unofficial, reverse-engineered protocol. Because
|
||||
> most Apple IDs have two-factor authentication enabled, you **must** generate
|
||||
> an [app-specific password](https://support.apple.com/en-us/102654) and use
|
||||
> it as `ICLOUD_PASSWORD`.
|
||||
|
||||
### Notification System
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Internationalization (i18n) & Localization (l10n) Guide
|
||||
|
||||
DocuElevate supports **10 languages** for its web UI, with automatic browser
|
||||
language detection, user-preference persistence, and an AI-powered fallback
|
||||
translator for strings that haven't been manually translated yet.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Code | Language | Native Name | Priority |
|
||||
|------|------------|-------------|----------|
|
||||
| `en` | English | English | Tier 1 |
|
||||
| `de` | German | Deutsch | Tier 1 |
|
||||
| `fr` | French | Français | Tier 1 |
|
||||
| `es` | Spanish | Español | Tier 1 |
|
||||
| `it` | Italian | Italiano | Tier 1 |
|
||||
| `pt` | Portuguese | Português | Tier 1 |
|
||||
| `nl` | Dutch | Nederlands | Tier 2 |
|
||||
| `pl` | Polish | Polski | Tier 2 |
|
||||
| `zh` | Chinese | 中文 | Tier 2 |
|
||||
| `ru` | Russian | Русский | Tier 2 |
|
||||
|
||||
> **Tier 1** languages (European priority) have complete, manually-reviewed
|
||||
> translations. **Tier 2** languages have complete translations but may
|
||||
> receive less frequent updates.
|
||||
|
||||
## How Language Is Detected
|
||||
|
||||
DocuElevate resolves the display language in the following priority order:
|
||||
|
||||
1. **User profile preference** — stored in the database (`UserProfile.preferred_language`)
|
||||
and loaded into the session on login
|
||||
2. **Cookie** — `docuelevate_lang` cookie (30-day expiry, set when user selects a language)
|
||||
3. **Browser `Accept-Language` header** — the highest-priority match among supported languages
|
||||
4. **Default** — English (`en`)
|
||||
|
||||
## Selecting Your Language
|
||||
|
||||
### Via the Navigation Bar
|
||||
|
||||
Click the 🌐 **globe icon** in the top navigation bar. A dropdown menu shows all
|
||||
available languages with their native names and flag emoji. The current language
|
||||
is highlighted with a blue checkmark.
|
||||
|
||||
### Via the API
|
||||
|
||||
```bash
|
||||
# Set language to German
|
||||
curl -X POST http://localhost:8000/api/i18n/language \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"language": "de"}'
|
||||
|
||||
# List all available languages
|
||||
curl http://localhost:8000/api/i18n/languages
|
||||
```
|
||||
|
||||
### Via Cookie (Programmatic)
|
||||
|
||||
Set the `docuelevate_lang` cookie to any supported language code:
|
||||
|
||||
```javascript
|
||||
document.cookie = "docuelevate_lang=fr; max-age=2592000; path=/";
|
||||
location.reload();
|
||||
```
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation File Structure
|
||||
|
||||
Translations are stored as flat JSON files in `frontend/translations/`:
|
||||
|
||||
```
|
||||
frontend/translations/
|
||||
├── en.json # English (base / reference)
|
||||
├── de.json # German
|
||||
├── fr.json # French
|
||||
├── es.json # Spanish
|
||||
├── it.json # Italian
|
||||
├── pt.json # Portuguese
|
||||
├── nl.json # Dutch
|
||||
├── pl.json # Polish
|
||||
├── zh.json # Chinese
|
||||
└── ru.json # Russian
|
||||
```
|
||||
|
||||
Each file is a flat key-value dictionary with dot-notation namespacing:
|
||||
|
||||
```json
|
||||
{
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Upload",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"footer.copyright": "DocuElevate {year}"
|
||||
}
|
||||
```
|
||||
|
||||
### Using Translations in Templates
|
||||
|
||||
The `_()` function is available globally in all Jinja2 templates:
|
||||
|
||||
```jinja2
|
||||
{# Simple translation #}
|
||||
<h1>{{ _("dashboard.title") }}</h1>
|
||||
|
||||
{# Translation with placeholders #}
|
||||
<p>{{ _("upload.max_size", size="50 MB") }}</p>
|
||||
|
||||
{# Translation in attributes #}
|
||||
<button aria-label="{{ _('common.save') }}">{{ _("common.save") }}</button>
|
||||
```
|
||||
|
||||
### Using Translations in Python
|
||||
|
||||
```python
|
||||
from app.utils.i18n import translate
|
||||
|
||||
# Basic translation
|
||||
text = translate("nav.dashboard", "de") # → "Übersicht"
|
||||
|
||||
# With placeholders
|
||||
text = translate("footer.copyright", "fr", year="2025") # → "DocuElevate 2025"
|
||||
```
|
||||
|
||||
### Localization Helpers
|
||||
|
||||
Format dates, times, and numbers according to locale conventions:
|
||||
|
||||
```jinja2
|
||||
{# In templates — locale is automatically detected #}
|
||||
<span>{{ format_date_l10n(document.created_at) }}</span>
|
||||
<span>{{ format_number_l10n(file_count) }}</span>
|
||||
```
|
||||
|
||||
```python
|
||||
# In Python
|
||||
from app.utils.i18n import format_date, format_number
|
||||
|
||||
format_date(date(2025, 3, 15), "de") # → "15. March 2025"
|
||||
format_date(date(2025, 3, 15), "de", short=True) # → "15.03.2025"
|
||||
format_number(1234567, "de") # → "1.234.567"
|
||||
format_number(1234.56, "en") # → "1,234.56"
|
||||
```
|
||||
|
||||
### Adding a New Translation Key
|
||||
|
||||
1. Add the key and English text to `frontend/translations/en.json`
|
||||
2. Add translations for all other languages in their respective files
|
||||
3. Use `{{ _("your.new.key") }}` in templates
|
||||
|
||||
### AI Fallback Translation
|
||||
|
||||
When a translation key exists in English but not in the target language,
|
||||
DocuElevate can use the configured AI provider (OpenAI, Anthropic, etc.)
|
||||
to translate the string on-the-fly:
|
||||
|
||||
```python
|
||||
from app.utils.i18n import translate_with_ai_fallback
|
||||
|
||||
# Falls back to AI if no manual translation exists
|
||||
translated = translate_with_ai_fallback("Welcome to our platform", "de")
|
||||
```
|
||||
|
||||
The AI fallback:
|
||||
- Uses the `AI_MODEL` or `OPENAI_MODEL` setting
|
||||
- Caches results in memory for the process lifetime
|
||||
- Returns the original English text if the AI call fails
|
||||
- Is designed for graceful degradation — the UI never breaks
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
1. Create a new JSON file in `frontend/translations/` (e.g., `ja.json`)
|
||||
2. Copy the structure from `en.json` and translate all values
|
||||
3. Add the language to `SUPPORTED_LANGUAGES` in `app/utils/i18n.py`:
|
||||
```python
|
||||
{"code": "ja", "name": "Japanese", "native": "日本語", "flag": "🇯🇵"},
|
||||
```
|
||||
4. Add locale formatting rules to `_LOCALE_FORMATS` in the same file
|
||||
5. Create a database migration if needed (the `preferred_language` column
|
||||
already accepts any string up to 10 characters)
|
||||
|
||||
### Database Migration
|
||||
|
||||
Migration `027_add_user_language_preference` adds a `preferred_language`
|
||||
column to the `user_profiles` table. This column stores the user's chosen
|
||||
UI language as an ISO 639-1 code (e.g., `"de"`, `"fr"`). A `NULL` value
|
||||
means "auto-detect from browser settings."
|
||||
|
||||
### API Reference
|
||||
|
||||
#### `GET /api/i18n/languages`
|
||||
|
||||
Returns all supported languages and the current active language.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"languages": [
|
||||
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
|
||||
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"}
|
||||
],
|
||||
"current": "en",
|
||||
"default": "en"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/i18n/language`
|
||||
|
||||
Set the preferred UI language. Persists in session, cookie, and database.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"language": "de"}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"language": "de",
|
||||
"message": "Language changed to Deutsch"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The i18n system works out of the box
|
||||
with the default English language and automatically detects browser preferences.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Browser `Accept-Language` | Auto-detected | Used when no explicit preference is set |
|
||||
| `docuelevate_lang` cookie | Not set | Set when user selects a language via the UI |
|
||||
| `UserProfile.preferred_language` | `NULL` | Stored in DB for authenticated users |
|
||||
@@ -0,0 +1,252 @@
|
||||
# Mobile App
|
||||
|
||||
DocuElevate includes a native mobile application for iOS and Android built with **React Native** and **Expo**. The app allows users to capture documents with the device camera, pick files from the device storage, and receive push notifications when documents finish processing.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | iOS | Android |
|
||||
|---------|-----|---------|
|
||||
| SSO login (OAuth2) | ✅ | ✅ |
|
||||
| Local / basic auth login | ✅ | ✅ |
|
||||
| Auto-generated API token | ✅ | ✅ |
|
||||
| Camera capture → upload | ✅ | ✅ |
|
||||
| File picker upload | ✅ | ✅ |
|
||||
| Share Sheet / Share Intent | ✅ | ✅ |
|
||||
| Push notifications | ✅ | ✅ |
|
||||
| Document list | ✅ | ✅ |
|
||||
| Dark mode | ✅ | ✅ |
|
||||
|
||||
## Getting Started (Development)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18 or later
|
||||
- [Expo CLI](https://docs.expo.dev/get-started/installation/): `npm install -g @expo/cli`
|
||||
- [Expo Go](https://expo.dev/client) app on your iOS or Android device (for development)
|
||||
- A running DocuElevate server reachable from your device
|
||||
|
||||
### Run in development mode
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
npm install
|
||||
npx expo start
|
||||
```
|
||||
|
||||
Scan the QR code with **Expo Go** on your device. On iOS you can also use the Camera app.
|
||||
|
||||
## Building for Production
|
||||
|
||||
DocuElevate uses **Expo Application Services (EAS)** to produce App Store / Play Store binaries.
|
||||
|
||||
```bash
|
||||
# Install EAS CLI globally
|
||||
npm install -g eas-cli
|
||||
|
||||
# Authenticate with Expo
|
||||
eas login
|
||||
|
||||
# Build for iOS (requires Apple Developer account)
|
||||
eas build --platform ios
|
||||
|
||||
# Build for Android
|
||||
eas build --platform android
|
||||
```
|
||||
|
||||
See the [EAS Build documentation](https://docs.expo.dev/build/introduction/) for full setup instructions.
|
||||
|
||||
## Authentication
|
||||
|
||||
### SSO Login Flow
|
||||
|
||||
The mobile app uses the server's existing OAuth2/SSO setup:
|
||||
|
||||
1. User enters the DocuElevate server URL on the login screen.
|
||||
2. The app opens `<server>/login?mobile=1&redirect_uri=docuelevate://callback` in the **system browser** (Safari / Chrome).
|
||||
3. The user authenticates via SSO or local credentials.
|
||||
4. The server redirects back to `docuelevate://callback`.
|
||||
5. The app calls `POST /api/mobile/generate-token` to exchange the session for a **long-lived API token**.
|
||||
6. The token is stored securely in the device's keychain (`expo-secure-store`).
|
||||
|
||||
### Auto-generated Mobile Token
|
||||
|
||||
When the mobile app completes login it automatically creates a named API token (`"Mobile App – <device name>"`) via `POST /api/mobile/generate-token`. This token:
|
||||
|
||||
- Works identically to tokens created manually in the web UI.
|
||||
- Is shown in the **API Tokens** page (`/api-tokens`) and can be revoked there.
|
||||
- Is stored in the device's secure keychain, never in plain storage.
|
||||
|
||||
## Push Notifications
|
||||
|
||||
Push notifications are delivered via the **Expo Push Notification** service, which routes through Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android.
|
||||
|
||||
**No server-side APNs/FCM credentials are required** – Expo's servers handle the provider integration.
|
||||
|
||||
### How it works
|
||||
|
||||
1. After login, the app requests notification permission from the operating system.
|
||||
2. If granted, the app obtains an **Expo Push Token** (`ExponentPushToken[…]`).
|
||||
3. The token is registered with the backend via `POST /api/mobile/register-device`.
|
||||
4. When a document finishes processing, the server sends a push notification to all registered devices for that user.
|
||||
|
||||
### Managing registered devices
|
||||
|
||||
Users can see and remove their registered devices from the **Profile** tab in the app, or via the API:
|
||||
|
||||
```bash
|
||||
# List registered devices
|
||||
curl -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices
|
||||
|
||||
# Remove a device
|
||||
curl -X DELETE -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices/<id>
|
||||
```
|
||||
|
||||
## Uploading Documents
|
||||
|
||||
### Camera Capture
|
||||
|
||||
1. Open the **Upload** tab.
|
||||
2. Tap **Camera**.
|
||||
3. Point the camera at the document and take a photo.
|
||||
4. The image is immediately uploaded and queued for processing.
|
||||
|
||||
### File Picker
|
||||
|
||||
1. Open the **Upload** tab.
|
||||
2. Tap **File Picker**.
|
||||
3. Browse to and select one or more files (PDF, DOCX, images, etc.).
|
||||
4. Files are uploaded and queued for processing.
|
||||
|
||||
### Share Sheet (iOS) / Share Intent (Android)
|
||||
|
||||
The app registers itself as a share target so any file can be sent directly to DocuElevate from another app:
|
||||
|
||||
1. Open a file in Files, Mail, Safari, or any other app.
|
||||
2. Tap the **Share** button (iOS) or **Share** (Android).
|
||||
3. Find and tap **DocuElevate** in the share sheet.
|
||||
4. The file is immediately uploaded.
|
||||
|
||||
> **Note:** The app must be installed on the device for it to appear in the share sheet.
|
||||
|
||||
## Mobile API Endpoints
|
||||
|
||||
The backend exposes a dedicated `/api/mobile/` namespace:
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| `POST` | `/api/mobile/generate-token` | Session | Exchange SSO session for API token |
|
||||
| `POST` | `/api/mobile/register-device` | Bearer | Register Expo push token |
|
||||
| `GET` | `/api/mobile/devices` | Bearer | List registered devices |
|
||||
| `DELETE` | `/api/mobile/devices/{id}` | Bearer | Deactivate a device |
|
||||
| `GET` | `/api/mobile/whoami` | Bearer | Get current user profile |
|
||||
|
||||
All other API endpoints (file upload, file listing, etc.) work with Bearer token authentication.
|
||||
|
||||
### POST /api/mobile/generate-token
|
||||
|
||||
Exchanges an active web session (cookie) for a permanent API token suitable for use in the mobile app.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "device_name": "John's iPhone" }
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"token": "de_AbCdEfGhIjKl...",
|
||||
"token_id": 42,
|
||||
"name": "Mobile App – John's iPhone",
|
||||
"created_at": "2026-03-10T09:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ The `token` value is returned **once only**. Store it in the device's secure keychain immediately.
|
||||
|
||||
### POST /api/mobile/register-device
|
||||
|
||||
Registers an Expo push token for the authenticated user.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"push_token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
|
||||
"device_name": "John's iPhone",
|
||||
"platform": "ios"
|
||||
}
|
||||
```
|
||||
|
||||
Supported platforms: `ios`, `android`, `web`.
|
||||
|
||||
Re-registering the same token is safe (idempotent).
|
||||
|
||||
### GET /api/mobile/whoami
|
||||
|
||||
Returns the current user's profile.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"owner_id": "john@example.com",
|
||||
"display_name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/...",
|
||||
"is_admin": false
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
No server-side configuration is required to enable the mobile app. The Expo push notification routing does not need FCM or APNs credentials on the server.
|
||||
|
||||
If you wish to use **direct FCM/APNs** without Expo's relay, replace the `send_expo_push_notification` function in `app/utils/push_notification.py` with your own implementation.
|
||||
|
||||
## Project Structure (mobile/)
|
||||
|
||||
```
|
||||
mobile/
|
||||
├── App.tsx # Root component
|
||||
├── app.json # Expo/EAS configuration
|
||||
├── eas.json # EAS Build profiles
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── context/
|
||||
│ └── AuthContext.tsx # Auth state + SSO login flow
|
||||
├── hooks/
|
||||
│ └── usePushNotifications.ts # Push token registration
|
||||
├── screens/
|
||||
│ ├── LoginScreen.tsx # Server URL + SSO button
|
||||
│ ├── UploadScreen.tsx # Camera capture + file picker
|
||||
│ ├── FilesScreen.tsx # Processed document list
|
||||
│ └── ProfileScreen.tsx # User profile + sign out
|
||||
└── services/
|
||||
└── api.ts # DocuElevate REST API client
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication was cancelled or failed"
|
||||
|
||||
- Ensure the server URL is correct (including `https://`).
|
||||
- Verify the server is reachable from your device's network.
|
||||
- Confirm that `AUTH_ENABLED=True` on the server.
|
||||
|
||||
### Push notifications not arriving
|
||||
|
||||
1. Check that the app has notification permission (Settings → DocuElevate → Notifications).
|
||||
2. Verify the device is registered: `GET /api/mobile/devices`.
|
||||
3. Ensure the server can reach `https://exp.host` (outbound HTTPS on port 443).
|
||||
4. On Android, add `google-services.json` to the `mobile/` directory if you are building your own binary.
|
||||
|
||||
### "Connection refused" or timeout
|
||||
|
||||
- Verify that the DocuElevate server is running and accessible.
|
||||
- Ensure the server's `EXTERNAL_HOSTNAME` or reverse proxy is configured correctly.
|
||||
- Check that the server accepts CORS requests from `docuelevate://`.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [API Documentation](./API.md)
|
||||
- [Configuration Guide](./ConfigurationGuide.md)
|
||||
- [Deployment Guide](./DeploymentGuide.md)
|
||||
@@ -0,0 +1,375 @@
|
||||
# Social Login Setup Guide
|
||||
|
||||
This guide explains how to configure social login providers (Google, Microsoft, Apple, Dropbox) for DocuElevate. Social login lets your users sign in with their existing accounts, reducing friction and eliminating the need for separate passwords.
|
||||
|
||||
## Overview
|
||||
|
||||
DocuElevate supports four social login providers:
|
||||
|
||||
| Provider | Protocol | Best For |
|
||||
|----------|----------|----------|
|
||||
| **Google** | OAuth2 / OpenID Connect | Consumers and Google Workspace organizations |
|
||||
| **Microsoft** | OAuth2 / OpenID Connect | Microsoft 365 / Azure AD organizations and personal Microsoft accounts |
|
||||
| **Apple** | OAuth2 / OpenID Connect | iOS/macOS users, privacy-focused users |
|
||||
| **Dropbox** | OAuth2 | Teams already using Dropbox as a storage destination |
|
||||
|
||||
Each provider is **independently enabled** — you can use one, several, or all of them at the same time. Social login works alongside any other DocuElevate authentication method (simple auth, OIDC/Authentik, local signup).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before configuring any social login provider, ensure:
|
||||
|
||||
1. **Authentication is enabled**: `AUTH_ENABLED=true` in your `.env` file
|
||||
2. **Session secret is set**: `SESSION_SECRET` must be a random string of at least 32 characters
|
||||
3. **HTTPS is configured**: All social login providers require HTTPS redirect URIs in production. Use a reverse proxy (Traefik, Nginx, Caddy) with a valid TLS certificate
|
||||
4. **External hostname is set**: `EXTERNAL_HOSTNAME` must match your public domain (e.g., `docuelevate.example.com`)
|
||||
|
||||
> **Note:** Social login users are regular (non-admin) users by default. To grant admin access, use the Admin Panel (**Settings → User Management**) after the user's first login, or configure admin groups via Authentik/OIDC.
|
||||
|
||||
## Callback URLs
|
||||
|
||||
Each social login provider uses a callback URL to redirect users back to DocuElevate after authentication. The callback URL pattern is:
|
||||
|
||||
```
|
||||
https://<EXTERNAL_HOSTNAME>/social-callback/<provider>
|
||||
```
|
||||
|
||||
For example, if your DocuElevate instance is at `https://docuelevate.example.com`:
|
||||
|
||||
| Provider | Callback URL |
|
||||
|----------|-------------|
|
||||
| Google | `https://docuelevate.example.com/social-callback/google` |
|
||||
| Microsoft | `https://docuelevate.example.com/social-callback/microsoft` |
|
||||
| Apple | `https://docuelevate.example.com/social-callback/apple` |
|
||||
| Dropbox | `https://docuelevate.example.com/social-callback/dropbox` |
|
||||
|
||||
---
|
||||
|
||||
## Google Sign-In
|
||||
|
||||
### 1. Create OAuth Credentials in Google Cloud Console
|
||||
|
||||
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project (or select an existing one)
|
||||
3. Navigate to **APIs & Services → Credentials**
|
||||
4. Click **Create Credentials → OAuth client ID**
|
||||
5. If prompted, configure the **OAuth consent screen** first:
|
||||
- **User Type**: External (or Internal for Google Workspace)
|
||||
- **App name**: DocuElevate
|
||||
- **User support email**: Your email
|
||||
- **Authorized domains**: Your domain (e.g., `example.com`)
|
||||
- **Scopes**: Add `email`, `profile`, and `openid`
|
||||
6. Back on the Credentials page, create an **OAuth 2.0 Client ID**:
|
||||
- **Application type**: Web application
|
||||
- **Name**: DocuElevate
|
||||
- **Authorized redirect URIs**: `https://docuelevate.example.com/social-callback/google`
|
||||
7. Note the **Client ID** and **Client Secret**
|
||||
|
||||
### 2. Configure DocuElevate
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_GOOGLE_ENABLED=true
|
||||
SOCIAL_AUTH_GOOGLE_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com
|
||||
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret-here
|
||||
```
|
||||
|
||||
### 3. Restart DocuElevate
|
||||
|
||||
```bash
|
||||
docker compose restart api worker
|
||||
```
|
||||
|
||||
### Google-Specific Notes
|
||||
|
||||
- **Google Workspace**: If you want to restrict sign-in to users in your Google Workspace organization, set the OAuth consent screen to "Internal"
|
||||
- **Verification**: Google may require app verification if you're using External user type and requesting sensitive scopes. For small teams (<100 users), you can add test users instead
|
||||
- **Unified Auth**: If you also use Google Drive as a storage destination, users who sign in with Google will already be authenticated with a Google identity — simplifying the Google Drive integration experience
|
||||
|
||||
---
|
||||
|
||||
## Microsoft Sign-In (Azure AD / Microsoft Entra ID)
|
||||
|
||||
### 1. Register an Application in Azure
|
||||
|
||||
1. Go to the [Azure Portal](https://portal.azure.com/)
|
||||
2. Navigate to **Microsoft Entra ID → App registrations**
|
||||
3. Click **New registration**
|
||||
4. Fill in:
|
||||
- **Name**: DocuElevate
|
||||
- **Supported account types**: Choose based on your needs:
|
||||
- *Accounts in this organizational directory only* — single-tenant (your org only)
|
||||
- *Accounts in any organizational directory* — multi-tenant
|
||||
- *Accounts in any organizational directory and personal Microsoft accounts* — broadest reach
|
||||
- **Redirect URI**: Select **Web** and enter `https://docuelevate.example.com/social-callback/microsoft`
|
||||
5. Click **Register**
|
||||
6. Note the **Application (client) ID**
|
||||
7. Navigate to **Certificates & secrets → New client secret**
|
||||
8. Add a description and expiration, then click **Add**
|
||||
9. Note the **Value** (this is your client secret — it's only shown once!)
|
||||
|
||||
### 2. Configure API Permissions
|
||||
|
||||
1. In your app registration, go to **API permissions**
|
||||
2. Ensure these permissions are present (they're usually added by default):
|
||||
- `openid`
|
||||
- `profile`
|
||||
- `email`
|
||||
3. Click **Grant admin consent** if you're a tenant admin
|
||||
|
||||
### 3. Configure DocuElevate
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_MICROSOFT_ENABLED=true
|
||||
SOCIAL_AUTH_MICROSOFT_CLIENT_ID=12345678-abcd-efgh-ijkl-123456789012
|
||||
SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET=your~client~secret~value
|
||||
SOCIAL_AUTH_MICROSOFT_TENANT=common
|
||||
```
|
||||
|
||||
**Tenant options:**
|
||||
|
||||
| Value | Who Can Sign In |
|
||||
|-------|----------------|
|
||||
| `common` | Any Microsoft account (personal + any Azure AD organization) |
|
||||
| `organizations` | Any Azure AD organization (work/school accounts only) |
|
||||
| `consumers` | Personal Microsoft accounts only (outlook.com, hotmail.com, etc.) |
|
||||
| `<tenant-id>` | Only users in a specific Azure AD tenant (use the GUID from Azure Portal) |
|
||||
|
||||
### 4. Restart DocuElevate
|
||||
|
||||
```bash
|
||||
docker compose restart api worker
|
||||
```
|
||||
|
||||
### Microsoft-Specific Notes
|
||||
|
||||
- **Client secret expiration**: Azure AD client secrets expire (max 2 years). Set a calendar reminder to rotate them before they expire
|
||||
- **Conditional Access**: If your organization uses Azure AD Conditional Access policies, social login will respect them
|
||||
- **Unified Auth**: If you also use OneDrive as a storage destination, users who sign in with Microsoft will already have a Microsoft identity — potentially simplifying OneDrive integration
|
||||
|
||||
---
|
||||
|
||||
## Apple Sign-In
|
||||
|
||||
Apple Sign-In requires an Apple Developer account ($99/year) and more setup than other providers.
|
||||
|
||||
### 1. Configure in Apple Developer Portal
|
||||
|
||||
1. Go to the [Apple Developer Portal](https://developer.apple.com/account/)
|
||||
2. Navigate to **Certificates, Identifiers & Profiles → Identifiers**
|
||||
3. Click **+** and select **App IDs** → Register an App ID:
|
||||
- **Description**: DocuElevate
|
||||
- **Bundle ID**: e.g., `com.example.docuelevate`
|
||||
- Enable **Sign In with Apple** capability
|
||||
4. Click **+** again and select **Services IDs**:
|
||||
- **Description**: DocuElevate Web
|
||||
- **Identifier**: e.g., `com.example.docuelevate.web` (this is your Client ID)
|
||||
- Enable **Sign In with Apple**
|
||||
- Click **Configure** next to Sign In with Apple:
|
||||
- **Primary App ID**: Select the App ID created above
|
||||
- **Domains**: `docuelevate.example.com`
|
||||
- **Return URLs**: `https://docuelevate.example.com/social-callback/apple`
|
||||
5. Click **Save** and **Continue** → **Register**
|
||||
6. Navigate to **Keys** → Click **+** to create a new key:
|
||||
- **Key Name**: DocuElevate Sign-In
|
||||
- Enable **Sign In with Apple**
|
||||
- Click **Configure** and select the App ID created above
|
||||
- Click **Continue** → **Register**
|
||||
- **Download the private key file** (`.p8`) — you can only download it once!
|
||||
- Note the **Key ID**
|
||||
7. Note your **Team ID** (shown in the top-right corner of the Developer Portal)
|
||||
|
||||
### 2. Configure DocuElevate
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_APPLE_ENABLED=true
|
||||
SOCIAL_AUTH_APPLE_CLIENT_ID=com.example.docuelevate.web
|
||||
SOCIAL_AUTH_APPLE_TEAM_ID=ABCDE12345
|
||||
SOCIAL_AUTH_APPLE_KEY_ID=FGHIJ67890
|
||||
SOCIAL_AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg...
|
||||
...your key content here...
|
||||
-----END PRIVATE KEY-----"
|
||||
```
|
||||
|
||||
> **Tip:** You can also store the private key as a single line with `\n` for line breaks:
|
||||
> ```bash
|
||||
> SOCIAL_AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMG...\n-----END PRIVATE KEY-----"
|
||||
> ```
|
||||
|
||||
### 3. Restart DocuElevate
|
||||
|
||||
```bash
|
||||
docker compose restart api worker
|
||||
```
|
||||
|
||||
### Apple-Specific Notes
|
||||
|
||||
- **Email relay**: Apple offers a "Hide My Email" feature that provides a relay email address (e.g., `abc123@privaterelay.appleid.com`). DocuElevate accepts these addresses
|
||||
- **First login only**: Apple sends the user's name only on the very first authorization. If the user revokes and re-authorizes, their name may not be sent again
|
||||
- **Developer account required**: You need an Apple Developer account ($99/year) to use Sign In with Apple
|
||||
- **Key rotation**: Apple private keys don't expire, but if you suspect compromise, revoke the key in the Developer Portal and create a new one
|
||||
|
||||
---
|
||||
|
||||
## Dropbox Sign-In
|
||||
|
||||
### 1. Create a Dropbox App
|
||||
|
||||
1. Go to the [Dropbox App Console](https://www.dropbox.com/developers/apps)
|
||||
2. Click **Create app**
|
||||
3. Choose:
|
||||
- **API**: Scoped access
|
||||
- **Access type**: Full Dropbox (or App folder, depending on your needs)
|
||||
- **Name**: DocuElevate Auth (or reuse your existing Dropbox storage app)
|
||||
4. In the app settings, go to the **OAuth 2** section:
|
||||
- Add **Redirect URI**: `https://docuelevate.example.com/social-callback/dropbox`
|
||||
5. Note the **App key** (this is your Client ID) and **App secret** (this is your Client Secret)
|
||||
|
||||
> **Tip:** If you already have a Dropbox app configured for DocuElevate's storage integration, you can reuse the same app — just add the social login redirect URI. Alternatively, create a separate app for authentication to keep concerns separated.
|
||||
|
||||
### 2. Configure DocuElevate
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_DROPBOX_ENABLED=true
|
||||
SOCIAL_AUTH_DROPBOX_CLIENT_ID=your_dropbox_app_key
|
||||
SOCIAL_AUTH_DROPBOX_CLIENT_SECRET=your_dropbox_app_secret
|
||||
```
|
||||
|
||||
### 3. Restart DocuElevate
|
||||
|
||||
```bash
|
||||
docker compose restart api worker
|
||||
```
|
||||
|
||||
### Dropbox-Specific Notes
|
||||
|
||||
- **Unified Auth**: If you also use Dropbox as a storage destination, authenticating via Dropbox establishes the user's Dropbox identity — making it easier to manage Dropbox storage integration
|
||||
- **App review**: Dropbox may require app review for production apps with more than 50 users. See [Dropbox App Review](https://www.dropbox.com/developers/reference/developer-guide#app-review)
|
||||
- **Personal vs. Business**: The same app works for both personal Dropbox and Dropbox Business accounts
|
||||
|
||||
---
|
||||
|
||||
## Unified Authentication and Storage
|
||||
|
||||
One of the key advantages of social login in DocuElevate is the potential for **unified authentication** — using the same identity for both signing in and accessing cloud storage destinations:
|
||||
|
||||
| Social Login Provider | Related Storage Destination | Benefit |
|
||||
|---|---|---|
|
||||
| Google | Google Drive | User already has a Google identity for Drive integration |
|
||||
| Microsoft | OneDrive | User already has a Microsoft identity for OneDrive integration |
|
||||
| Dropbox | Dropbox | User already has a Dropbox identity for Dropbox integration |
|
||||
| Apple | *(none)* | Provides a familiar, privacy-respecting login option |
|
||||
|
||||
When a user signs in with a social provider that matches a configured storage destination, the administrator can leverage the same OAuth credentials or simplify the integration setup. Note that the storage integration credentials are configured separately in the admin settings — social login establishes the user's identity, not their storage permissions.
|
||||
|
||||
## Combining Multiple Auth Methods
|
||||
|
||||
DocuElevate supports running multiple authentication methods simultaneously:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Login Page │
|
||||
├──────────────────────────────────────────────────┤
|
||||
│ Username / Password form (always shown) │
|
||||
│ │
|
||||
│ ─── Or continue with ─── │
|
||||
│ │
|
||||
│ [Authentik SSO] (if OIDC configured) │
|
||||
│ [Sign in with Google] (if Google enabled) │
|
||||
│ [Sign in with Microsoft] (if Microsoft enabled) │
|
||||
│ [Sign in with Apple] (if Apple enabled) │
|
||||
│ [Sign in with Dropbox] (if Dropbox enabled) │
|
||||
│ │
|
||||
│ [Create account] (if local signup enabled) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
All methods create or update the same `UserProfile` record, so a user is consistently identified regardless of how they sign in.
|
||||
|
||||
## Admin Management
|
||||
|
||||
Social login users appear in the **Admin → User Management** panel like any other user. Admins can:
|
||||
|
||||
- View which provider a user authenticated with
|
||||
- Block or unblock social login users
|
||||
- Set upload limits and subscription tiers
|
||||
- Grant admin privileges (social login users are never automatically admin)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **HTTPS is required**: All social login providers require HTTPS callback URLs in production
|
||||
2. **Credentials are sensitive**: Store client secrets securely — use environment variables, never commit them to source control
|
||||
3. **Least privilege**: Only request the scopes you need (DocuElevate requests `openid`, `profile`, and `email`)
|
||||
4. **Rotate secrets**: Set calendar reminders to rotate OAuth client secrets before they expire (especially Microsoft, which has a max 2-year expiration)
|
||||
5. **Monitor logins**: Check the DocuElevate audit log for unusual login patterns
|
||||
6. **Social login users are not admins**: Admin access must be explicitly granted by an existing admin
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Unknown social provider" error**
|
||||
- The provider is not enabled or credentials are missing
|
||||
- Check that `SOCIAL_AUTH_<PROVIDER>_ENABLED=true` is set
|
||||
- Verify client ID and secret are configured
|
||||
|
||||
2. **"Could not retrieve email from provider" error**
|
||||
- The provider didn't return an email address
|
||||
- For Google: Ensure `email` scope is included (it is by default)
|
||||
- For Apple: User may have chosen "Hide My Email" — this is expected and should still work
|
||||
- For Dropbox: Ensure the app has permission to read the user's email
|
||||
|
||||
3. **Redirect URI mismatch**
|
||||
- The callback URL registered with the provider must exactly match what DocuElevate generates
|
||||
- Check your `EXTERNAL_HOSTNAME` setting
|
||||
- Ensure you're using HTTPS in production
|
||||
- The callback URL format is: `https://<EXTERNAL_HOSTNAME>/social-callback/<provider>`
|
||||
|
||||
4. **"Social login failed" error**
|
||||
- Check DocuElevate logs (`docker compose logs api`) for detailed error messages
|
||||
- Verify the provider's OAuth app is not suspended or in development mode
|
||||
- For Google: Check if the OAuth consent screen needs verification
|
||||
- For Microsoft: Ensure admin consent was granted for the required permissions
|
||||
|
||||
5. **User can't log in after changing provider settings**
|
||||
- After changing social login configuration, restart DocuElevate: `docker compose restart api worker`
|
||||
- Social login settings require a restart to take effect (`restart_required: true`)
|
||||
|
||||
### Debug Checklist
|
||||
|
||||
- [ ] `AUTH_ENABLED=true` is set
|
||||
- [ ] `SESSION_SECRET` is at least 32 characters
|
||||
- [ ] `EXTERNAL_HOSTNAME` matches your public domain
|
||||
- [ ] Provider-specific `_ENABLED=true` is set
|
||||
- [ ] Client ID and secret are correctly configured (no extra spaces)
|
||||
- [ ] Callback URL is registered with the provider
|
||||
- [ ] HTTPS is working on your domain
|
||||
- [ ] DocuElevate has been restarted after configuration changes
|
||||
|
||||
## Environment Variable Reference
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `SOCIAL_AUTH_GOOGLE_ENABLED` | No | Enable Google Sign-In (`true`/`false`). Default: `false` |
|
||||
| `SOCIAL_AUTH_GOOGLE_CLIENT_ID` | When Google enabled | Google OAuth2 client ID |
|
||||
| `SOCIAL_AUTH_GOOGLE_CLIENT_SECRET` | When Google enabled | Google OAuth2 client secret |
|
||||
| `SOCIAL_AUTH_MICROSOFT_ENABLED` | No | Enable Microsoft Sign-In (`true`/`false`). Default: `false` |
|
||||
| `SOCIAL_AUTH_MICROSOFT_CLIENT_ID` | When Microsoft enabled | Azure AD application (client) ID |
|
||||
| `SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET` | When Microsoft enabled | Azure AD client secret |
|
||||
| `SOCIAL_AUTH_MICROSOFT_TENANT` | No | Azure AD tenant. Default: `common` |
|
||||
| `SOCIAL_AUTH_APPLE_ENABLED` | No | Enable Apple Sign-In (`true`/`false`). Default: `false` |
|
||||
| `SOCIAL_AUTH_APPLE_CLIENT_ID` | When Apple enabled | Apple Services ID |
|
||||
| `SOCIAL_AUTH_APPLE_TEAM_ID` | When Apple enabled | Apple Developer Team ID |
|
||||
| `SOCIAL_AUTH_APPLE_KEY_ID` | When Apple enabled | Apple Sign-In key ID |
|
||||
| `SOCIAL_AUTH_APPLE_PRIVATE_KEY` | When Apple enabled | Apple Sign-In private key (PEM) |
|
||||
| `SOCIAL_AUTH_DROPBOX_ENABLED` | No | Enable Dropbox Sign-In (`true`/`false`). Default: `false` |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_ID` | When Dropbox enabled | Dropbox App Key |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_SECRET` | When Dropbox enabled | Dropbox App Secret |
|
||||
@@ -348,6 +348,7 @@ in task messages or logs.
|
||||
| `PAPERLESS` | Paperless-ngx REST API, API token |
|
||||
| `EMAIL` | SMTP/STARTTLS, file as attachment |
|
||||
| `RCLONE` | `rclone copyto` subprocess, per-user rclone config |
|
||||
| `ICLOUD` | pyicloud library, Apple ID + app-specific password |
|
||||
|
||||
### Multiple Destinations
|
||||
|
||||
|
||||
@@ -63,6 +63,54 @@ DocuElevate will process the following attachment types from emails:
|
||||
| TIFF | `.tif`, `.tiff` | Common format from older scanners/fax |
|
||||
| Multi-page TIFF | `.tif` | Full multi-page support |
|
||||
|
||||
### Controlling Which Attachment Types Are Ingested
|
||||
|
||||
By default, DocuElevate only ingests **document** attachments (PDFs, Word, Excel, PowerPoint, OpenDocument, RTF, TXT, CSV, HTML, Markdown). Images are **not** ingested by default — this prevents cluttering your document archive with inline images or unrelated photo attachments.
|
||||
|
||||
#### Global Default (Admin Setting)
|
||||
|
||||
Set the `IMAP_ATTACHMENT_FILTER` environment variable to control the system-wide fallback when no ingestion profile is assigned to a mailbox:
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| `documents_only` | **(Default)** Only PDFs and office/document files. Images are skipped. |
|
||||
| `all` | All supported file types, including images. |
|
||||
|
||||
```env
|
||||
IMAP_ATTACHMENT_FILTER=documents_only
|
||||
```
|
||||
|
||||
#### Ingestion Profiles (Fine-Grained Per-Mailbox Control)
|
||||
|
||||
For precise control, you can create **Ingestion Profiles** that let you pick exactly which file-type categories to accept from each mailbox. This is more powerful than the binary global toggle and works independently per mailbox.
|
||||
|
||||
**Available categories:**
|
||||
|
||||
| Category | File types included |
|
||||
|----------|---------------------|
|
||||
| PDF | `.pdf` |
|
||||
| Microsoft Office | `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, and macro-enabled variants |
|
||||
| OpenDocument | `.odt`, `.ods`, `.odp`, `.odg`, `.odf` (LibreOffice / OpenOffice) |
|
||||
| Text & Data | `.txt`, `.csv`, `.rtf` |
|
||||
| Web & Markup | `.html`, `.htm`, `.md`, `.markdown` |
|
||||
| Images | `.jpg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg` |
|
||||
|
||||
**Managing profiles:**
|
||||
|
||||
1. Go to **Email Ingestion** (`/imap-accounts`)
|
||||
2. Click **Manage profiles** (or the **+** icon next to the profile dropdown)
|
||||
3. Create a new profile, give it a name, and tick the categories you want
|
||||
4. When adding or editing a mailbox, select your profile from the dropdown
|
||||
|
||||
Two built-in profiles are always available and cannot be deleted:
|
||||
|
||||
- **Documents Only** — PDF, Office, OpenDocument, Text, Web (no images)
|
||||
- **All Files** — all categories including images
|
||||
|
||||
Users can also create unlimited **custom profiles** to mix and match exactly the categories they need per mailbox (e.g. a scanner mailbox that only accepts PDFs, or a finance mailbox that accepts Office and CSV but not images).
|
||||
|
||||
Custom profiles are created via the UI or the `/api/imap-profiles/` API.
|
||||
|
||||
---
|
||||
|
||||
## Setting Up Your Scanner/Device
|
||||
|
||||
@@ -206,6 +206,9 @@ function _makeMenuLink(href, iconClass, label, extraClasses = '') {
|
||||
// Links section
|
||||
const linksDiv = document.createElement('div');
|
||||
linksDiv.className = 'py-1';
|
||||
linksDiv.appendChild(
|
||||
_makeMenuLink('/profile', 'fas fa-user-circle text-blue-400', 'Profile Settings', 'text-gray-700')
|
||||
);
|
||||
linksDiv.appendChild(
|
||||
_makeMenuLink('/subscription', 'fas fa-layer-group text-indigo-400', 'My Subscription', 'text-gray-700')
|
||||
);
|
||||
@@ -272,6 +275,18 @@ function _makeMenuLink(href, iconClass, label, extraClasses = '') {
|
||||
userRow.appendChild(mUserInfo);
|
||||
mobileAuthSection.appendChild(userRow);
|
||||
|
||||
// Profile Settings link
|
||||
const profileLink = document.createElement('a');
|
||||
profileLink.href = '/profile';
|
||||
profileLink.className =
|
||||
'flex items-center px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50';
|
||||
const profileIcon = document.createElement('i');
|
||||
profileIcon.className = 'fas fa-user-circle mr-2 text-blue-400';
|
||||
profileIcon.setAttribute('aria-hidden', 'true');
|
||||
profileLink.appendChild(profileIcon);
|
||||
profileLink.appendChild(document.createTextNode('Profile Settings'));
|
||||
mobileAuthSection.appendChild(profileLink);
|
||||
|
||||
// Subscription link
|
||||
const subLink = document.createElement('a');
|
||||
subLink.href = '/subscription';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}404 - Not Found{% endblock %}
|
||||
{% block title %}{{ _("error.404_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col items-center justify-center text-center px-4 py-16">
|
||||
<div class="max-w-xl">
|
||||
<h1 class="text-7xl font-extrabold text-blue-600 mb-6">404</h1>
|
||||
<h2 class="text-3xl font-bold text-gray-800 mb-4">Oops, we couldn’t find that page!</h2>
|
||||
<h1 class="text-7xl font-extrabold text-blue-600 mb-6">{{ _("error.404_code") }}</h1>
|
||||
<h2 class="text-3xl font-bold text-gray-800 mb-4">{{ _("error.404_heading") }}</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.
|
||||
{{ _("error.404_message") }}
|
||||
</p>
|
||||
<a href="/" class="inline-block bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
|
||||
← Return Home
|
||||
← {{ _("error.404_home") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Server Error - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("error.500_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="relative isolate px-6 pt-14 lg:px-8">
|
||||
@@ -7,27 +7,27 @@
|
||||
<div class="mx-auto max-w-3xl py-12">
|
||||
<img
|
||||
src="/static/explosion.jpg"
|
||||
alt="Illustration of a server error"
|
||||
alt="{{ _('error.500_img_alt') }}"
|
||||
class="mx-auto rounded-xl shadow-xl h-[300px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<h1 class="text-7xl font-bold text-red-500">500</h1>
|
||||
<h1 class="text-7xl font-bold text-red-500">{{ _("error.500_code") }}</h1>
|
||||
<p class="mt-4 text-4xl font-semibold tracking-tight text-gray-900">
|
||||
Oops! Something Went Wrong.
|
||||
{{ _("error.500_heading") }}
|
||||
</p>
|
||||
<p class="mt-4 text-lg leading-8 text-gray-600">
|
||||
Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?
|
||||
{{ _("error.500_message1") }}
|
||||
</p>
|
||||
<p class="mt-2 text-lg leading-8 text-gray-600">
|
||||
We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.
|
||||
{{ _("error.500_message2") }}
|
||||
</p>
|
||||
|
||||
{% if exc %}
|
||||
<details class="mt-8 bg-gray-100 rounded-lg p-4 text-left shadow-md">
|
||||
<summary class="cursor-pointer font-medium text-blue-600 hover:text-blue-700">
|
||||
Show Debug Info
|
||||
{{ _("error.500_debug_info") }}
|
||||
</summary>
|
||||
<pre class="mt-4 text-sm text-gray-700 overflow-auto">{{ exc }}</pre>
|
||||
</details>
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<div class="mt-10">
|
||||
<a href="/" class="inline-block bg-blue-600 px-6 py-3 rounded-md text-white font-semibold shadow-md hover:bg-blue-700 transition-colors">
|
||||
← Go Home
|
||||
← {{ _("error.500_home") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,69 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}About DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("about.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-4xl font-bold mb-4">About DocuElevate</h1>
|
||||
<h1 class="text-4xl font-bold mb-4">{{ _("about.heading") }}</h1>
|
||||
<p class="text-gray-700 mb-6 leading-relaxed">
|
||||
Welcome to <strong>DocuElevate</strong> – your modern, intelligent solution for document processing!
|
||||
We've built DocuElevate to completely transform the way you handle your documents – from upload
|
||||
to extraction, from processing to storage.
|
||||
{{ _("about.intro_pre") }}<strong>{{ _("app.name") }}</strong>{{ _("about.intro_post") }}
|
||||
</p>
|
||||
|
||||
<!-- Our Story Section -->
|
||||
<section class="bg-white shadow rounded p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-2">Our Story</h2>
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ _("about.story_heading") }}</h2>
|
||||
<p class="text-gray-600 mb-4">
|
||||
DocuElevate was created with one goal in mind: to simplify and streamline document management
|
||||
for everyone, whether you're a small startup or a large enterprise.
|
||||
{{ _("about.story_p1") }}
|
||||
</p>
|
||||
<p class="text-gray-600">
|
||||
We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama,
|
||||
OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly
|
||||
with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document
|
||||
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
|
||||
{{ _("about.story_p2") }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Key Features Section - Updated with more comprehensive features -->
|
||||
<section class="bg-white shadow rounded p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-2">Key Features</h2>
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ _("about.features_heading") }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 class="font-medium text-lg text-gray-800">Document Processing</h3>
|
||||
<h3 class="font-medium text-lg text-gray-800">{{ _("about.features_processing_heading") }}</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 ml-2">
|
||||
<li>Simple and secure file uploads with drag & drop support</li>
|
||||
<li>OCR powered by Azure Document Intelligence</li>
|
||||
<li>Automated metadata extraction using a pluggable AI provider</li>
|
||||
<li>PDF conversion for various file formats via Gotenberg</li>
|
||||
<li>Intelligent document classification and date extraction</li>
|
||||
<li>{{ _("about.features_processing_upload") }}</li>
|
||||
<li>{{ _("about.features_processing_ocr") }}</li>
|
||||
<li>{{ _("about.features_processing_metadata") }}</li>
|
||||
<li>{{ _("about.features_processing_pdf") }}</li>
|
||||
<li>{{ _("about.features_processing_classification") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg text-gray-800">Integration & Storage</h3>
|
||||
<h3 class="font-medium text-lg text-gray-800">{{ _("about.features_integration_heading") }}</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 ml-2">
|
||||
<li>Multi-destination support (store documents in multiple locations)</li>
|
||||
<li>Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3</li>
|
||||
<li>Self-hosted options: Nextcloud, Paperless NGX, WebDAV</li>
|
||||
<li>Transfer protocols: FTP, SFTP, Email forwarding</li>
|
||||
<li>{{ _("about.features_integration_multi_dest") }}</li>
|
||||
<li>{{ _("about.features_integration_cloud") }}</li>
|
||||
<li>{{ _("about.features_integration_selfhosted") }}</li>
|
||||
<li>{{ _("about.features_integration_protocols") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg text-gray-800">Automation</h3>
|
||||
<h3 class="font-medium text-lg text-gray-800">{{ _("about.features_automation_heading") }}</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 ml-2">
|
||||
<li>IMAP inbox polling from multiple sources</li>
|
||||
<li>Gmail and generic email account integration</li>
|
||||
<li>Automated document ingestion from various inputs</li>
|
||||
<li>Background processing with Redis and Celery</li>
|
||||
<li>{{ _("about.features_automation_imap") }}</li>
|
||||
<li>{{ _("about.features_automation_gmail") }}</li>
|
||||
<li>{{ _("about.features_automation_ingestion") }}</li>
|
||||
<li>{{ _("about.features_automation_background") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg text-gray-800">Administration</h3>
|
||||
<h3 class="font-medium text-lg text-gray-800">{{ _("about.features_admin_heading") }}</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 ml-2">
|
||||
<li>Powerful REST API for programmatic access</li>
|
||||
<li>OAuth2 authentication support with Authentik</li>
|
||||
<li>Highly configurable via environment variables</li>
|
||||
<li>Docker-ready for easy deployment and scaling</li>
|
||||
<li>{{ _("about.features_admin_api") }}</li>
|
||||
<li>{{ _("about.features_admin_oauth2") }}</li>
|
||||
<li>{{ _("about.features_admin_config") }}</li>
|
||||
<li>{{ _("about.features_admin_docker") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,43 +65,43 @@
|
||||
|
||||
<!-- Meet the Creator Section -->
|
||||
<section class="bg-white shadow rounded p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-2">Meet the Creator</h2>
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ _("about.creator_heading") }}</h2>
|
||||
<p class="text-gray-600">
|
||||
DocuElevate is passionately developed by
|
||||
<a href="https://www.christianlouis.de" target="_blank" class="text-blue-600 hover:underline">Christian Krakau-Louis</a>.
|
||||
{{ _("about.creator_pre") }}
|
||||
<a href="https://www.christianlouis.de" target="_blank" class="text-blue-600 hover:underline">{{ _("about.creator_name") }}</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Privacy & Legal Section -->
|
||||
<section class="bg-white shadow rounded p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-2">Privacy & Legal</h2>
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ _("about.legal_heading") }}</h2>
|
||||
<p class="text-gray-600 mb-4">
|
||||
We care about your privacy and data security. Please review our:
|
||||
{{ _("about.legal_description") }}
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a href="/privacy" class="text-blue-600 hover:underline">Privacy Notice</a>
|
||||
<a href="/license" class="text-blue-600 hover:underline">License Information</a>
|
||||
<a href="/privacy" class="text-blue-600 hover:underline">{{ _("about.legal_privacy") }}</a>
|
||||
<a href="/license" class="text-blue-600 hover:underline">{{ _("about.legal_license") }}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Links Section -->
|
||||
<section class="bg-white shadow rounded p-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">Get Involved</h2>
|
||||
<h2 class="text-2xl font-semibold mb-2">{{ _("about.involved_heading") }}</h2>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Want to dive into the code, contribute ideas, or learn more about DocuElevate?
|
||||
{{ _("about.involved_description") }}
|
||||
</p>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<a href="https://github.com/christianlouis/DocuElevate" target="_blank" class="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
|
||||
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Logo" class="h-6 w-6" />
|
||||
<span>View DocuElevate on GitHub</span>
|
||||
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="{{ _('about.github_logo_alt') }}" class="h-6 w-6" />
|
||||
<span>{{ _("about.involved_github") }}</span>
|
||||
</a>
|
||||
<a href="https://www.docuelevate.org" target="_blank" class="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-globe h-6 w-6"></i>
|
||||
<span>Visit DocuElevate Website</span>
|
||||
<span>{{ _("about.involved_website") }}</span>
|
||||
</a>
|
||||
<a href="https://docuelevate.readthedocs.io" target="_blank" class="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-book h-6 w-6"></i>
|
||||
<span>Read the Documentation</span>
|
||||
<span>{{ _("about.involved_docs") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}API Tokens – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("api_tokens.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="apiTokens()" x-init="loadTokens()" class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<i class="fas fa-key text-yellow-500" aria-hidden="true"></i>
|
||||
API Tokens
|
||||
{{ _("api_tokens.heading") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400 text-sm leading-relaxed max-w-2xl">
|
||||
Create personal API tokens to interact with the DocuElevate API programmatically.
|
||||
Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload
|
||||
or retrieve documents.
|
||||
{{ _("api_tokens.intro") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Create token section -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="create-token-heading">
|
||||
<h2 id="create-token-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Create New Token</h2>
|
||||
<h2 id="create-token-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("api_tokens.create_heading") }}</h2>
|
||||
<form @submit.prevent="createToken()" class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="flex-1">
|
||||
<label for="token-name" class="sr-only">Token name</label>
|
||||
<label for="token-name" class="sr-only">{{ _("api_tokens.token_name_label") }}</label>
|
||||
<input
|
||||
id="token-name"
|
||||
type="text"
|
||||
x-model="newTokenName"
|
||||
placeholder="e.g. CI Pipeline, Webhook Upload, My Script"
|
||||
placeholder="{{ _('api_tokens.token_name_placeholder') }}"
|
||||
required
|
||||
minlength="1"
|
||||
maxlength="255"
|
||||
@@ -44,7 +42,7 @@
|
||||
style="min-height:40px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i>
|
||||
<span x-text="creating ? 'Creating…' : 'Create Token'"></span>
|
||||
<span x-text="creating ? '{{ _('api_tokens.creating') }}' : '{{ _('api_tokens.create_token') }}'"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -54,9 +52,9 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-0.5 text-lg" aria-hidden="true"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">Token created successfully!</p>
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">{{ _("api_tokens.token_created") }}</p>
|
||||
<p class="text-green-700 dark:text-green-300 text-xs mt-1">
|
||||
Copy this token now — it will <strong>not be shown again</strong>.
|
||||
{{ _("api_tokens.copy_warning_1") }} <strong>{{ _("api_tokens.copy_warning_2") }}</strong>.
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code
|
||||
@@ -72,10 +70,10 @@
|
||||
hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500
|
||||
transition-colors"
|
||||
style="min-height:40px; min-width:44px;"
|
||||
:aria-label="copied ? 'Copied!' : 'Copy token to clipboard'"
|
||||
:aria-label="copied ? '{{ _('common.copied') }}' : '{{ _('api_tokens.copy_to_clipboard') }}'"
|
||||
>
|
||||
<i :class="copied ? 'fas fa-check text-green-600' : 'fas fa-copy'" aria-hidden="true"></i>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? 'Copied!' : 'Copy'"></span>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? '{{ _('common.copied') }}' : '{{ _('common.copy') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,11 +86,11 @@
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="usage-heading">
|
||||
<h2 id="usage-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
<i class="fas fa-code text-blue-500 mr-2" aria-hidden="true"></i>
|
||||
Usage Example
|
||||
{{ _("api_tokens.usage_heading") }}
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-3">
|
||||
Use your API token in the <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">Authorization</code>
|
||||
header with any API request:
|
||||
{{ _("api_tokens.usage_intro_pre") }} <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">Authorization</code>
|
||||
{{ _("api_tokens.usage_intro_post") }}
|
||||
</p>
|
||||
<div class="relative">
|
||||
<pre class="bg-gray-900 text-green-400 rounded-lg p-4 text-sm overflow-x-auto font-mono leading-relaxed"><code>curl -X POST "<span x-text="baseUrl"></span>/api/files/ui-upload" \
|
||||
@@ -104,7 +102,7 @@
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-gray-700 text-gray-300 rounded text-xs hover:bg-gray-600
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors"
|
||||
style="min-height:30px; min-width:30px;"
|
||||
aria-label="Copy upload example to clipboard"
|
||||
aria-label="{{ _('api_tokens.copy_upload_example') }}"
|
||||
>
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -114,14 +112,14 @@
|
||||
<!-- Tokens list -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden" aria-labelledby="tokens-heading">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 id="tokens-heading" class="text-lg font-semibold text-gray-900 dark:text-white">Your Tokens</h2>
|
||||
<h2 id="tokens-heading" class="text-lg font-semibold text-gray-900 dark:text-white">{{ _("api_tokens.your_tokens") }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<template x-if="loading">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin text-2xl mb-2" aria-hidden="true"></i>
|
||||
<p class="text-sm">Loading tokens…</p>
|
||||
<p class="text-sm">{{ _("api_tokens.loading_tokens") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,24 +127,24 @@
|
||||
<template x-if="!loading && tokens.length === 0">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-key text-4xl mb-3 text-gray-300 dark:text-gray-600" aria-hidden="true"></i>
|
||||
<p class="font-medium">No API tokens yet</p>
|
||||
<p class="text-sm mt-1">Create your first token above to get started.</p>
|
||||
<p class="font-medium">{{ _("api_tokens.no_tokens_heading") }}</p>
|
||||
<p class="text-sm mt-1">{{ _("api_tokens.no_tokens_help") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Tokens table -->
|
||||
<template x-if="!loading && tokens.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm" aria-label="API Tokens">
|
||||
<table class="w-full text-sm" aria-label="{{ _('api_tokens.table_aria') }}">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-750 text-left">
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Name</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Token Prefix</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Created</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Last Used</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Last IP</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Status</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider sr-only">Actions</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_name") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_prefix") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_created") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_last_used") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_last_ip") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider sr-only">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -159,7 +157,7 @@
|
||||
<code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-xs font-mono" x-text="token.token_prefix + '…'"></code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="formatDate(token.created_at)"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="token.last_used_at ? formatDate(token.last_used_at) : 'Never'"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="token.last_used_at ? formatDate(token.last_used_at) : '{{ _('api_tokens.never') }}'"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
<code x-show="token.last_used_ip" class="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-xs font-mono" x-text="token.last_used_ip"></code>
|
||||
<span x-show="!token.last_used_ip" class="text-gray-400">—</span>
|
||||
@@ -168,7 +166,7 @@
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="token.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'"
|
||||
x-text="token.is_active ? 'Active' : 'Revoked'"
|
||||
x-text="token.is_active ? '{{ _('api_tokens.status_active') }}' : '{{ _('api_tokens.status_revoked') }}'"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
@@ -181,10 +179,10 @@
|
||||
dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-50 transition-colors"
|
||||
style="min-height:36px; min-width:44px;"
|
||||
:aria-label="'Revoke token ' + token.name"
|
||||
:aria-label="'{{ _('api_tokens.revoke_prefix') }} ' + token.name"
|
||||
>
|
||||
<i :class="revoking === token.id ? 'fas fa-spinner fa-spin' : 'fas fa-trash-alt'" class="mr-1" aria-hidden="true"></i>
|
||||
Revoke
|
||||
{{ _("api_tokens.revoke") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _("audit.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="auditLogs()" x-init="fetchLogs()" class="container mx-auto px-4 py-8 max-w-7xl">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
<i class="fas fa-shield-halved mr-2 text-indigo-600" aria-hidden="true"></i>{{ _("audit.title") }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ _("audit.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 md:mt-0 flex items-center gap-3">
|
||||
{% if siem_enabled %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100"
|
||||
title="{{ _('audit.siem_enabled_title') ~ (siem_transport|upper) }}">
|
||||
<i class="fas fa-tower-broadcast mr-1" aria-hidden="true"></i>SIEM: {{ siem_transport|upper }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
title="{{ _('audit.siem_disabled_title') }}">
|
||||
<i class="fas fa-tower-broadcast mr-1" aria-hidden="true"></i>{{ _("audit.siem_off") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<button @click="fetchLogs()" class="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600 min-h-[44px] min-w-[44px]"
|
||||
aria-label="{{ _('audit.refresh_label') }}">
|
||||
<i class="fas fa-arrows-rotate mr-1" aria-hidden="true"></i>{{ _("common.refresh") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-4 mb-6" aria-label="{{ _('audit.filters_section_label') }}">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label for="filter-action" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _("audit.filter_action") }}</label>
|
||||
<select id="filter-action" x-model="filters.action" @change="fetchLogs()"
|
||||
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
|
||||
<option value="">{{ _("audit.filter_all_actions") }}</option>
|
||||
<template x-for="a in distinctActions" :key="a">
|
||||
<option :value="a" x-text="a"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-user" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _("audit.filter_user") }}</label>
|
||||
<select id="filter-user" x-model="filters.user" @change="fetchLogs()"
|
||||
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
|
||||
<option value="">{{ _("audit.filter_all_users") }}</option>
|
||||
<template x-for="u in distinctUsers" :key="u">
|
||||
<option :value="u" x-text="u"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-severity" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _("audit.filter_severity") }}</label>
|
||||
<select id="filter-severity" x-model="filters.severity" @change="fetchLogs()"
|
||||
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
|
||||
<option value="">{{ _("common.all") }}</option>
|
||||
<option value="info">{{ _("common.info") }}</option>
|
||||
<option value="warning">{{ _("common.warning") }}</option>
|
||||
<option value="error">{{ _("common.error") }}</option>
|
||||
<option value="critical">{{ _("audit.critical") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-resource" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{{ _("audit.filter_resource_type") }}</label>
|
||||
<input id="filter-resource" type="text" x-model.debounce.300ms="filters.resource_type" @input="fetchLogs()"
|
||||
placeholder="{{ _('audit.filter_resource_placeholder') }}"
|
||||
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm min-h-[44px]">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results summary -->
|
||||
<div class="flex items-center justify-between mb-3 text-sm text-gray-500 dark:text-gray-400" aria-live="polite">
|
||||
<span x-show="!loading" x-text="total + ' event' + (total !== 1 ? 's' : '') + ' found'"></span>
|
||||
<span x-show="loading"><i class="fas fa-spinner fa-spin mr-1" aria-hidden="true"></i>{{ _("common.loading") }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" aria-label="{{ _('audit.table_label') }}">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.col_timestamp") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.filter_severity") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.filter_user") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.filter_action") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.col_resource") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("audit.col_ip") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ _("common.details") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<template x-for="e in entries" :key="e.id">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-600 dark:text-gray-300" x-text="formatTs(e.timestamp)"></td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs">
|
||||
<span :class="severityClass(e.severity)" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium" x-text="e.severity"></span>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-700 dark:text-gray-200" x-text="e.user"></td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs font-mono text-indigo-700 dark:text-indigo-300" x-text="e.action"></td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-600 dark:text-gray-300">
|
||||
<span x-text="e.resource_type || '—'"></span>
|
||||
<span x-show="e.resource_id" class="text-gray-400" x-text="' #' + e.resource_id"></span>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-500 dark:text-gray-400" x-text="e.ip_address || '—'"></td>
|
||||
<td class="px-4 py-2 text-xs text-gray-500 dark:text-gray-400 max-w-xs truncate" :title="e.details ? JSON.stringify(e.details) : ''" x-text="e.details ? JSON.stringify(e.details) : '—'"></td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr x-show="!loading && entries.length === 0">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||
<i class="fas fa-inbox text-3xl mb-2" aria-hidden="true"></i>
|
||||
<p class="text-lg">{{ _("audit.no_events") }}</p>
|
||||
<p class="text-sm mt-1">{{ _("audit.no_events_hint") }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav class="flex items-center justify-between mt-4" aria-label="{{ _('audit.pagination_label') }}" x-show="total > filters.limit">
|
||||
<button @click="prevPage()" :disabled="filters.offset === 0"
|
||||
class="px-3 py-1.5 rounded-md text-sm border border-gray-300 dark:border-gray-600 disabled:opacity-50 min-h-[44px] min-w-[44px]"
|
||||
aria-label="{{ _('audit.prev_label') }}">
|
||||
<i class="fas fa-chevron-left mr-1" aria-hidden="true"></i>{{ _("audit.prev") }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400" x-text="'Page ' + currentPage + ' of ' + totalPages"></span>
|
||||
<button @click="nextPage()" :disabled="filters.offset + filters.limit >= total"
|
||||
class="px-3 py-1.5 rounded-md text-sm border border-gray-300 dark:border-gray-600 disabled:opacity-50 min-h-[44px] min-w-[44px]"
|
||||
aria-label="{{ _('audit.next_label') }}">
|
||||
{{ _("audit.next") }}<i class="fas fa-chevron-right ml-1" aria-hidden="true"></i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function auditLogs() {
|
||||
return {
|
||||
entries: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
distinctActions: [],
|
||||
distinctUsers: [],
|
||||
filters: {
|
||||
action: '',
|
||||
user: '',
|
||||
severity: '',
|
||||
resource_type: '',
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
},
|
||||
get currentPage() { return Math.floor(this.filters.offset / this.filters.limit) + 1; },
|
||||
get totalPages() { return Math.max(1, Math.ceil(this.total / this.filters.limit)); },
|
||||
|
||||
async fetchLogs() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.filters.action) params.set('action', this.filters.action);
|
||||
if (this.filters.user) params.set('user', this.filters.user);
|
||||
if (this.filters.severity) params.set('severity', this.filters.severity);
|
||||
if (this.filters.resource_type) params.set('resource_type', this.filters.resource_type);
|
||||
params.set('limit', this.filters.limit);
|
||||
params.set('offset', this.filters.offset);
|
||||
|
||||
const [logsResp, actionsResp, usersResp] = await Promise.all([
|
||||
fetch('/api/audit-logs?' + params.toString()),
|
||||
fetch('/api/audit-logs/actions'),
|
||||
fetch('/api/audit-logs/users'),
|
||||
]);
|
||||
if (logsResp.ok) {
|
||||
const data = await logsResp.json();
|
||||
this.entries = data.items;
|
||||
this.total = data.total;
|
||||
}
|
||||
if (actionsResp.ok) this.distinctActions = await actionsResp.json();
|
||||
if (usersResp.ok) this.distinctUsers = await usersResp.json();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
prevPage() {
|
||||
if (this.filters.offset > 0) {
|
||||
this.filters.offset = Math.max(0, this.filters.offset - this.filters.limit);
|
||||
this.fetchLogs();
|
||||
}
|
||||
},
|
||||
nextPage() {
|
||||
if (this.filters.offset + this.filters.limit < this.total) {
|
||||
this.filters.offset += this.filters.limit;
|
||||
this.fetchLogs();
|
||||
}
|
||||
},
|
||||
formatTs(iso) {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString();
|
||||
},
|
||||
severityClass(sev) {
|
||||
const map = {
|
||||
info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
error: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
critical: 'bg-red-200 text-red-900 dark:bg-red-800 dark:text-red-100',
|
||||
};
|
||||
return map[sev] || 'bg-gray-100 text-gray-800';
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Backup Management{% endblock %}
|
||||
{% block title %}{{ _("backup.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8" x-data="backupDashboard()">
|
||||
@@ -9,10 +9,10 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
<i class="fas fa-database mr-2 text-blue-600" aria-hidden="true"></i>
|
||||
Backup Management
|
||||
{{ _("backup.heading") }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).
|
||||
{{ _("backup.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@@ -21,26 +21,26 @@
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
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 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> Backup Now (Hourly)
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> {{ _("backup.trigger_hourly") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('daily')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
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 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> Backup Now (Daily)
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> {{ _("backup.trigger_daily") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('weekly')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
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 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> Backup Now (Weekly)
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> {{ _("backup.trigger_weekly") }}
|
||||
</button>
|
||||
<button @click="runCleanup()"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
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 disabled:opacity-50 min-h-[44px]"
|
||||
title="Run retention cleanup now">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> Clean Up
|
||||
title="{{ _('backup.cleanup_title') }}">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> {{ _("backup.clean_up") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,18 +60,18 @@
|
||||
role="dialog" aria-modal="true" :aria-labelledby="'confirmTitle'">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-sm w-full p-6"
|
||||
@keydown.escape.window="confirmOpen = false">
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">Confirm action</h2>
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">{{ _("backup.confirm_title") }}</h2>
|
||||
<p class="text-sm text-gray-600 mb-4" x-text="confirmMsg"></p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="confirmOpen = false; confirmResolve(false)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 min-h-[44px]">
|
||||
Cancel
|
||||
{{ _("common.cancel") }}
|
||||
</button>
|
||||
<button @click="confirmOpen = false; confirmResolve(true)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 min-h-[44px]">
|
||||
Confirm
|
||||
{{ _("backup.confirm_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,36 +81,36 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- Backup enabled -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Auto-backup</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_auto_backup") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold {% if backup_enabled %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> Enabled
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> Disabled{% endif %}
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("common.enabled") }}
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> {{ _("common.disabled") }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Remote destination -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Remote destination</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_remote_dest") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800">
|
||||
{% if backup_remote_destination %}
|
||||
<i class="fas fa-cloud-upload-alt mr-1 text-blue-500" aria-hidden="true"></i>
|
||||
{{ backup_remote_destination }}
|
||||
{% else %}
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> Local only</span>
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> {{ _("backup.local_only") }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Retention hourly -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Retention</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_retention") }}</p>
|
||||
<p class="mt-1 text-sm text-gray-700">
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> hourly ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> daily ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> weekly
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> {{ _("backup.type_hourly") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> {{ _("backup.type_daily") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> {{ _("backup.type_weekly") }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Total backup size -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Total local size</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_total_size") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800" id="totalSize">
|
||||
{{ (total_size / 1048576) | round(2) }} MB
|
||||
</p>
|
||||
@@ -120,17 +120,17 @@
|
||||
<!-- Upload restore section -->
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-1">
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>Restore from File
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>{{ _("backup.restore_heading") }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Upload a <code>.db.gz</code> backup archive to restore the database.
|
||||
<strong class="text-red-600">This will overwrite all current data.</strong>
|
||||
{{ _("backup.restore_desc_pre") }} <code>.db.gz</code> {{ _("backup.restore_desc_mid") }}
|
||||
<strong class="text-red-600">{{ _("backup.restore_desc_warning") }}</strong>
|
||||
</p>
|
||||
<form id="restoreForm" @submit.prevent="submitRestore" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-start sm:items-end">
|
||||
<div>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">Backup archive (.db.gz)</label>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">{{ _("backup.restore_file_label") }}</label>
|
||||
<input type="file" id="restoreFile" name="file" accept=".gz"
|
||||
required
|
||||
class="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
@@ -139,7 +139,7 @@
|
||||
:disabled="restoring"
|
||||
class="inline-flex items-center px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white text-sm font-medium rounded-md disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-undo mr-2" aria-hidden="true"></i>
|
||||
<span x-text="restoring ? 'Restoring…' : 'Restore'"></span>
|
||||
<span x-text="restoring ? '{{ _('backup.restoring') }}' : '{{ _('backup.restore_btn') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -149,30 +149,30 @@
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>Backup Archives
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} records)</span>
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>{{ _("backup.archives_heading") }}
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} {{ _("backup.records_label") }})</span>
|
||||
</h2>
|
||||
<!-- Tier filter -->
|
||||
<div class="flex gap-2 text-sm">
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">All</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Hourly</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Daily</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Weekly</button>
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("common.all") }}</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_hourly") }}</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_daily") }}</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_weekly") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if records %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="Backup archives">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="{{ _('backup.table_aria') }}">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Filename</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Storage</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_filename") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_type") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_created") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_size") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_storage") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -186,15 +186,15 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.backup_type == 'hourly' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> hourly
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> {{ _("backup.type_hourly") }}
|
||||
</span>
|
||||
{% elif r.backup_type == 'daily' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> daily
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> {{ _("backup.type_daily") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> weekly
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> {{ _("backup.type_weekly") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -209,7 +209,7 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.status == 'ok' %}
|
||||
<span class="inline-flex items-center text-green-700">
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> ok
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("backup.status_ok") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center text-red-600">
|
||||
@@ -220,7 +220,7 @@
|
||||
<td class="px-4 py-3 text-sm text-gray-600">
|
||||
{% if r.local_path %}
|
||||
<span class="inline-flex items-center" title="{{ r.local_path }}">
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> local
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> {{ _("backup.storage_local") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.remote_destination %}
|
||||
@@ -236,15 +236,15 @@
|
||||
{% if r.local_path %}
|
||||
<a href="/api/admin/backup/{{ r.id }}/download"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50 text-gray-700 min-h-[32px]"
|
||||
aria-label="Download backup {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> Download
|
||||
aria-label="{{ _('common.download') }} {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> {{ _("common.download") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button @click="deleteBackup({{ r.id }}, '{{ r.filename }}')"
|
||||
type="button"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-red-300 rounded hover:bg-red-50 text-red-600 ml-1 min-h-[32px]"
|
||||
aria-label="Delete backup {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Delete
|
||||
aria-label="{{ _('common.delete') }} {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> {{ _("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -255,8 +255,8 @@
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center text-gray-400">
|
||||
<i class="fas fa-database text-4xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-lg font-medium">No backups yet.</p>
|
||||
<p class="text-sm">Click <em>Backup Now</em> to create your first backup.</p>
|
||||
<p class="text-lg font-medium">{{ _("backup.no_backups") }}</p>
|
||||
<p class="text-sm">{{ _("backup.no_backups_hint_pre") }}<em>{{ _("backup.backup_now") }}</em>{{ _("backup.no_backups_hint_post") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -264,16 +264,15 @@
|
||||
<!-- Retention explanation -->
|
||||
<div class="mt-8 bg-gray-50 border border-gray-200 rounded-lg p-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> Retention policy
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> {{ _("backup.retention_heading") }}
|
||||
</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1 list-disc list-inside">
|
||||
<li><strong>Hourly</strong> – kept for 4 days ({{ backup_retain_hourly }} snapshots)</li>
|
||||
<li><strong>Daily</strong> – kept for 3 weeks ({{ backup_retain_daily }} snapshots)</li>
|
||||
<li><strong>Weekly</strong> – kept for ~3 months ({{ backup_retain_weekly }} snapshots)</li>
|
||||
<li><strong>{{ _("backup.type_hourly") }}</strong> {{ _("backup.retention_hourly_detail") }} ({{ backup_retain_hourly }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_daily") }}</strong> {{ _("backup.retention_daily_detail") }} ({{ backup_retain_daily }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_weekly") }}</strong> {{ _("backup.retention_weekly_detail") }} ({{ backup_retain_weekly }} {{ _("backup.snapshots") }})</li>
|
||||
</ul>
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
Backups beyond these limits are automatically pruned after each new backup is created.
|
||||
Use the <em>Clean Up</em> button to apply retention manually.
|
||||
{{ _("backup.retention_note_pre") }}<em>{{ _("backup.clean_up") }}</em>{{ _("backup.retention_note_post") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+149
-76
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-color-scheme-default="{{ ui_default_color_scheme | default('system') }}">
|
||||
<html lang="{{ current_locale | default('en') }}" data-color-scheme-default="{{ ui_default_color_scheme | default('system') }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{% block title %}DocuElevate{% endblock %}</title>
|
||||
@@ -37,10 +37,10 @@
|
||||
data-multi-user="{{ 'true' if multi_user_enabled else 'false' }}"
|
||||
data-allow-signup="{{ 'true' if allow_signup else 'false' }}">
|
||||
<!-- Skip to main content link for keyboard/screen reader users -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<a href="#main-content" class="skip-link">{{ _("nav.skip_to_content") }}</a>
|
||||
|
||||
<!-- Global Nav -->
|
||||
<nav class="bg-white shadow relative" aria-label="Main navigation">
|
||||
<nav class="bg-white shadow relative" aria-label="{{ _('nav.main_navigation') }}">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
|
||||
|
||||
<!-- Brand + Icon -->
|
||||
@@ -70,12 +70,12 @@
|
||||
<a href="/pricing"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/pricing' %}aria-current="page"{% endif %}>
|
||||
Pricing
|
||||
{{ _("nav.pricing") }}
|
||||
</a>
|
||||
<a href="/about"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
|
||||
About
|
||||
{{ _("nav.about") }}
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
@@ -84,32 +84,32 @@
|
||||
<a href="/"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-home mr-1 text-gray-400" aria-hidden="true"></i>Dashboard
|
||||
<i class="fas fa-home mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.dashboard") }}
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||
{% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-upload mr-1" aria-hidden="true"></i>Upload
|
||||
<i class="fas fa-upload mr-1" aria-hidden="true"></i>{{ _("nav.upload") }}
|
||||
</a>
|
||||
<a href="/files"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-folder-open mr-1 text-gray-400" aria-hidden="true"></i>Files
|
||||
<i class="fas fa-folder-open mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.files") }}
|
||||
</a>
|
||||
<a href="/search"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-search mr-1 text-gray-400" aria-hidden="true"></i>Search
|
||||
<i class="fas fa-search mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.search") }}
|
||||
</a>
|
||||
<a href="/pipelines"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-project-diagram mr-1 text-gray-400" aria-hidden="true"></i>Pipelines
|
||||
<i class="fas fa-project-diagram mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.pipelines") }}
|
||||
</a>
|
||||
<a href="/integrations"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-plug mr-1 text-gray-400" aria-hidden="true"></i>Integrations
|
||||
<i class="fas fa-plug mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.integrations") }}
|
||||
</a>
|
||||
|
||||
<!-- Admin dropdown – shown only for admin users via JS -->
|
||||
@@ -124,7 +124,7 @@
|
||||
:aria-expanded="adminMenuOpen"
|
||||
>
|
||||
<i class="fas fa-shield-alt mr-1 text-red-500" aria-hidden="true"></i>
|
||||
Admin
|
||||
{{ _("nav.admin") }}
|
||||
<i class="fas fa-chevron-down ml-1 text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div
|
||||
@@ -141,46 +141,52 @@
|
||||
>
|
||||
<div class="py-1">
|
||||
<a href="/settings" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> Settings
|
||||
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.settings") }}
|
||||
</a>
|
||||
<a href="/admin/users" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-users w-4 mr-2 text-blue-500" aria-hidden="true"></i> Users
|
||||
<i class="fas fa-users w-4 mr-2 text-blue-500" aria-hidden="true"></i> {{ _("nav.users") }}
|
||||
</a>
|
||||
<a href="/admin/plans" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-layer-group w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Plan Designer
|
||||
<i class="fas fa-layer-group w-4 mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.plan_designer") }}
|
||||
</a>
|
||||
<a href="/admin/credentials" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> Credentials
|
||||
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> {{ _("nav.credentials") }}
|
||||
</a>
|
||||
<a href="/admin/files" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> File Manager
|
||||
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.file_manager") }}
|
||||
</a>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/duplicates" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-clone w-4 mr-2 text-orange-500" aria-hidden="true"></i> Duplicates
|
||||
<i class="fas fa-clone w-4 mr-2 text-orange-500" aria-hidden="true"></i> {{ _("nav.duplicates") }}
|
||||
</a>
|
||||
<a href="/similarity" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-sitemap w-4 mr-2 text-purple-500" aria-hidden="true"></i> Similarity
|
||||
<i class="fas fa-sitemap w-4 mr-2 text-purple-500" aria-hidden="true"></i> {{ _("nav.similarity") }}
|
||||
</a>
|
||||
<a href="/admin/queue" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> Queue Monitor
|
||||
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> {{ _("nav.queue_monitor") }}
|
||||
</a>
|
||||
<a href="/admin/scheduled-jobs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-clock w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Scheduled Jobs
|
||||
<i class="fas fa-clock w-4 mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.scheduled_jobs") }}
|
||||
</a>
|
||||
<a href="/admin/compliance" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-check-double w-4 mr-2 text-green-500" aria-hidden="true"></i> Compliance
|
||||
</a>
|
||||
<a href="/admin/backup" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-database w-4 mr-2 text-green-600" aria-hidden="true"></i> Backup & Restore
|
||||
<i class="fas fa-database w-4 mr-2 text-green-600" aria-hidden="true"></i> {{ _("nav.backup_restore") }}
|
||||
</a>
|
||||
<a href="/admin/audit-logs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-shield-halved w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Audit Logs
|
||||
</a>
|
||||
<a href="/status" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> Status
|
||||
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.status") }}
|
||||
</a>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/admin/api-docs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> API Docs
|
||||
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> {{ _("nav.api_docs") }}
|
||||
</a>
|
||||
<a href="/developer-docs/" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> Developer Docs
|
||||
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> {{ _("nav.developer_docs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,18 +197,18 @@
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
aria-label="Help Center"
|
||||
title="Help Center"
|
||||
aria-label="{{ _('nav.help_center') }}"
|
||||
title="{{ _('nav.help_center') }}"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>Help
|
||||
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.help") }}
|
||||
</a>
|
||||
|
||||
<!-- Bell notification icon -->
|
||||
<a href="/notifications"
|
||||
id="notificationBell"
|
||||
class="relative p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="Notifications"
|
||||
title="Notifications"
|
||||
aria-label="{{ _('nav.notifications') }}"
|
||||
title="{{ _('nav.notifications') }}"
|
||||
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-bell" aria-hidden="true"></i>
|
||||
<span id="notificationBadge"
|
||||
@@ -210,14 +216,59 @@
|
||||
aria-live="polite"></span>
|
||||
</a>
|
||||
|
||||
<!-- Language selector dropdown -->
|
||||
<div x-data="{ langOpen: false }" class="relative">
|
||||
<button
|
||||
@click="langOpen = !langOpen"
|
||||
@click.outside="langOpen = false"
|
||||
type="button"
|
||||
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
title="{{ _('language.selector') }}"
|
||||
:aria-expanded="langOpen"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<i class="fas fa-globe"></i>
|
||||
</button>
|
||||
<div
|
||||
x-show="langOpen"
|
||||
x-transition:enter="transition ease-out duration-100 transform"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75 transform"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
|
||||
role="menu"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
>
|
||||
<div class="py-1">
|
||||
{% for lang in supported_languages %}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-left hover:bg-gray-100 {% if current_locale == lang.code %}bg-blue-50 text-blue-700 font-medium{% else %}text-gray-700{% endif %}"
|
||||
onclick="setLanguage('{{ lang.code }}')"
|
||||
>
|
||||
<span class="mr-2">{{ lang.flag }}</span>
|
||||
<span>{{ lang.native }}</span>
|
||||
{% if current_locale == lang.code %}
|
||||
<i class="fas fa-check ml-auto text-blue-500" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark mode toggle -->
|
||||
<button
|
||||
id="darkModeToggle"
|
||||
onclick="toggleDarkMode()"
|
||||
type="button"
|
||||
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="Toggle dark mode"
|
||||
title="Toggle dark mode"
|
||||
aria-label="{{ _('nav.toggle_dark_mode') }}"
|
||||
title="{{ _('nav.toggle_dark_mode') }}"
|
||||
>
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
@@ -232,10 +283,10 @@
|
||||
type="button"
|
||||
class="md:hidden inline-flex items-center justify-center p-3 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
style="min-height:44px;min-width:44px;"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-label="{{ _('nav.toggle_nav') }}"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<span class="sr-only">{{ _("nav.open_main_menu") }}</span>
|
||||
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
@@ -259,12 +310,12 @@
|
||||
<a href="/pricing"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/pricing' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-tag mr-2 text-gray-400" aria-hidden="true"></i>Pricing
|
||||
<i class="fas fa-tag mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.pricing") }}
|
||||
</a>
|
||||
<a href="/about"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-info-circle mr-2 text-gray-400" aria-hidden="true"></i>About
|
||||
<i class="fas fa-info-circle mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.about") }}
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
@@ -272,84 +323,87 @@
|
||||
<a href="/"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-home mr-2 text-gray-400" aria-hidden="true"></i>Dashboard
|
||||
<i class="fas fa-home mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.dashboard") }}
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||
{% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i>Upload
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i>{{ _("nav.upload") }}
|
||||
</a>
|
||||
<a href="/files"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>Files
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.files") }}
|
||||
</a>
|
||||
<a href="/search"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-search mr-2 text-gray-400" aria-hidden="true"></i>Search
|
||||
<i class="fas fa-search mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.search") }}
|
||||
</a>
|
||||
<a href="/pipelines"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-project-diagram mr-2 text-gray-400" aria-hidden="true"></i>Pipelines
|
||||
<i class="fas fa-project-diagram mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.pipelines") }}
|
||||
</a>
|
||||
<a href="/integrations"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-plug mr-2 text-gray-400" aria-hidden="true"></i>Integrations
|
||||
<i class="fas fa-plug mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.integrations") }}
|
||||
</a>
|
||||
<a href="/notifications"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-bell mr-2 text-gray-400" aria-hidden="true"></i>Notifications
|
||||
<i class="fas fa-bell mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.notifications") }}
|
||||
</a>
|
||||
|
||||
<!-- Admin section in mobile menu – shown only for admin users via JS -->
|
||||
<div id="mobileAdminSection" class="hidden">
|
||||
<div class="border-t border-gray-200 mt-1 pt-1">
|
||||
<p class="px-3 py-1 text-xs font-semibold text-red-600 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> Admin
|
||||
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> {{ _("nav.admin") }}
|
||||
</p>
|
||||
<a href="/settings" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> Settings
|
||||
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.settings") }}
|
||||
</a>
|
||||
<a href="/admin/users" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-users mr-2 text-blue-400" aria-hidden="true"></i> Users
|
||||
<i class="fas fa-users mr-2 text-blue-400" aria-hidden="true"></i> {{ _("nav.users") }}
|
||||
</a>
|
||||
<a href="/admin/plans" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-layer-group mr-2 text-indigo-400" aria-hidden="true"></i> Plan Designer
|
||||
<i class="fas fa-layer-group mr-2 text-indigo-400" aria-hidden="true"></i> {{ _("nav.plan_designer") }}
|
||||
</a>
|
||||
<a href="/admin/credentials" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> Credentials
|
||||
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> {{ _("nav.credentials") }}
|
||||
</a>
|
||||
<a href="/admin/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> File Manager
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.file_manager") }}
|
||||
</a>
|
||||
<a href="/duplicates" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-clone mr-2 text-orange-400" aria-hidden="true"></i> Duplicates
|
||||
<i class="fas fa-clone mr-2 text-orange-400" aria-hidden="true"></i> {{ _("nav.duplicates") }}
|
||||
</a>
|
||||
<a href="/similarity" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-sitemap mr-2 text-purple-400" aria-hidden="true"></i> Similarity
|
||||
<i class="fas fa-sitemap mr-2 text-purple-400" aria-hidden="true"></i> {{ _("nav.similarity") }}
|
||||
</a>
|
||||
<a href="/admin/queue" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> Queue Monitor
|
||||
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> {{ _("nav.queue_monitor") }}
|
||||
</a>
|
||||
<a href="/admin/scheduled-jobs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-clock mr-2 text-indigo-500" aria-hidden="true"></i> Scheduled Jobs
|
||||
<i class="fas fa-clock mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.scheduled_jobs") }}
|
||||
</a>
|
||||
<a href="/admin/compliance" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-check-double mr-2 text-green-500" aria-hidden="true"></i> Compliance
|
||||
</a>
|
||||
<a href="/admin/backup" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-database mr-2 text-green-500" aria-hidden="true"></i> Backup & Restore
|
||||
<i class="fas fa-database mr-2 text-green-500" aria-hidden="true"></i> {{ _("nav.backup_restore") }}
|
||||
</a>
|
||||
<a href="/status" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> Status
|
||||
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.status") }}
|
||||
</a>
|
||||
<a href="/admin/api-docs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> API Docs
|
||||
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> {{ _("nav.api_docs") }}
|
||||
</a>
|
||||
<a href="/developer-docs/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> Developer Docs
|
||||
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> {{ _("nav.developer_docs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,9 +413,9 @@
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
aria-label="Help Center"
|
||||
aria-label="{{ _('nav.help_center') }}"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>Help
|
||||
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.help") }}
|
||||
</a>
|
||||
|
||||
<!-- Dark mode toggle (mobile) -->
|
||||
@@ -370,10 +424,10 @@
|
||||
type="button"
|
||||
class="flex items-center w-full px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
id="darkModeToggleMobile"
|
||||
aria-label="Toggle dark mode"
|
||||
aria-label="{{ _('nav.toggle_dark_mode') }}"
|
||||
>
|
||||
<i class="fas fa-moon mr-2" id="darkModeIconMobile" aria-hidden="true"></i>
|
||||
<span id="darkModeTextMobile">Dark Mode</span>
|
||||
<span id="darkModeTextMobile">{{ _("nav.dark_mode") }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Auth / Account section (populated by common.js) -->
|
||||
@@ -394,14 +448,14 @@
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white shadow" role="contentinfo">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
|
||||
DocuElevate 2025 -
|
||||
<nav aria-label="Footer navigation" class="inline">
|
||||
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
|
||||
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
|
||||
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
|
||||
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
|
||||
<a href="/license" class="text-blue-500 hover:underline">License</a> -
|
||||
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a>
|
||||
{{ _("footer.copyright", year="2025") }} -
|
||||
<nav aria-label="{{ _('footer.navigation') }}" class="inline">
|
||||
<a href="/privacy" class="text-blue-500 hover:underline">{{ _("footer.privacy") }}</a> -
|
||||
<a href="/imprint" class="text-blue-500 hover:underline">{{ _("footer.imprint") }}</a> -
|
||||
<a href="/terms" class="text-blue-500 hover:underline">{{ _("footer.terms") }}</a> -
|
||||
<a href="/cookies" class="text-blue-500 hover:underline">{{ _("footer.cookies") }}</a> -
|
||||
<a href="/license" class="text-blue-500 hover:underline">{{ _("footer.license") }}</a> -
|
||||
<a href="/attribution" class="text-blue-500 hover:underline">{{ _("footer.attributions") }}</a>
|
||||
</nav> -
|
||||
<span class="text-xs">Version {{ app_version|default(version, true) }}{% if release_name %} "{{ release_name }}"{% endif %}</span>
|
||||
</div>
|
||||
@@ -416,19 +470,18 @@
|
||||
style="display:none!important"
|
||||
aria-live="polite"
|
||||
role="region"
|
||||
aria-label="Cookie notice">
|
||||
aria-label="{{ _('cookie.notice_label') }}">
|
||||
<p class="text-center sm:text-left">
|
||||
DocuElevate uses only essential session cookies required for authentication and service operation.
|
||||
No tracking or analytics cookies are used.
|
||||
<a href="/cookies" class="underline hover:text-blue-300 ml-1">Cookie Policy</a> ·
|
||||
<a href="/privacy" class="underline hover:text-blue-300 ml-1">Privacy Notice</a>
|
||||
{{ _("cookie.notice") }}
|
||||
<a href="/cookies" class="underline hover:text-blue-300 ml-1">{{ _("cookie.policy_link") }}</a> ·
|
||||
<a href="/privacy" class="underline hover:text-blue-300 ml-1">{{ _("cookie.privacy_link") }}</a>
|
||||
</p>
|
||||
<button
|
||||
id="cookieNoticeAccept"
|
||||
type="button"
|
||||
class="flex-shrink-0 bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-1.5 rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
aria-label="Acknowledge cookie notice">
|
||||
Got it
|
||||
aria-label="{{ _('cookie.accept') }}">
|
||||
{{ _("cookie.accept") }}
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
@@ -451,6 +504,26 @@
|
||||
<!-- Common JS (shared) -->
|
||||
<script src="/static/js/common.js"></script>
|
||||
|
||||
<!-- Language selector -->
|
||||
<script>
|
||||
function setLanguage(langCode) {
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
var headers = { 'Content-Type': 'application/json' };
|
||||
if (csrfToken && csrfToken.content) {
|
||||
headers['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
fetch('/api/i18n/language', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ language: langCode })
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function() {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Notification badge updater -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocuElevate - Subscription Activated</title>
|
||||
<title>{{ _("billing.success_page_title") }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
||||
@@ -12,7 +12,7 @@
|
||||
<body class="bg-gray-100 min-h-screen flex items-center justify-center py-8">
|
||||
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center" role="main">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
|
||||
<img src="/static/images/logo_writing.svg" alt="{{ _('auth.logo_alt') }}" class="h-16">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
@@ -21,9 +21,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">You're all set!</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">{{ _("billing.success_heading") }}</h1>
|
||||
<p class="text-gray-600 mb-8">
|
||||
Your subscription has been activated. Thank you for choosing DocuElevate!
|
||||
{{ _("billing.success_message") }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
@@ -31,13 +31,13 @@
|
||||
class="block w-full py-3 px-4 rounded-lg border-2 border-indigo-600 text-indigo-600 font-semibold hover:bg-indigo-50 transition focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
style="min-height:44px;display:flex;align-items:center;justify-content:center;"
|
||||
>
|
||||
<i class="fas fa-credit-card mr-2" aria-hidden="true"></i>Manage subscription
|
||||
<i class="fas fa-credit-card mr-2" aria-hidden="true"></i>{{ _("billing.manage_subscription") }}
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="block w-full py-3 px-4 rounded-lg bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
style="min-height:44px;display:flex;align-items:center;justify-content:center;"
|
||||
>
|
||||
<i class="fas fa-arrow-right mr-2" aria-hidden="true"></i>Go to dashboard
|
||||
<i class="fas fa-arrow-right mr-2" aria-hidden="true"></i>{{ _("billing.go_to_dashboard") }}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Compliance Templates - DocuElevate{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-6" x-data="complianceApp()">
|
||||
<!-- Header -->
|
||||
<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">Compliance Templates</h1>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Pre-built compliance configurations for GDPR, HIPAA, and SOC 2.
|
||||
Apply templates with one click to align your instance with regulatory requirements.
|
||||
</p>
|
||||
</div>
|
||||
<button @click="refreshAll()"
|
||||
class="inline-flex items-center 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-blue-500"
|
||||
:disabled="loading"
|
||||
aria-label="Refresh compliance status">
|
||||
<i class="fas fa-sync-alt mr-2" :class="loading ? 'animate-spin' : ''" aria-hidden="true"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Status Banner -->
|
||||
<div class="mb-6 rounded-lg p-5 shadow"
|
||||
:class="{
|
||||
'bg-green-50 border border-green-200': summary.overall_status === 'compliant',
|
||||
'bg-yellow-50 border border-yellow-200': summary.overall_status === 'partial',
|
||||
'bg-red-50 border border-red-200': summary.overall_status === 'non_compliant' || summary.overall_status === 'unknown'
|
||||
}">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-xl"
|
||||
:class="{
|
||||
'bg-green-100 text-green-600': summary.overall_status === 'compliant',
|
||||
'bg-yellow-100 text-yellow-600': summary.overall_status === 'partial',
|
||||
'bg-red-100 text-red-600': summary.overall_status === 'non_compliant' || summary.overall_status === 'unknown'
|
||||
}">
|
||||
<i :class="{
|
||||
'fas fa-check-circle': summary.overall_status === 'compliant',
|
||||
'fas fa-exclamation-triangle': summary.overall_status === 'partial',
|
||||
'fas fa-times-circle': summary.overall_status === 'non_compliant' || summary.overall_status === 'unknown'
|
||||
}" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold"
|
||||
:class="{
|
||||
'text-green-800': summary.overall_status === 'compliant',
|
||||
'text-yellow-800': summary.overall_status === 'partial',
|
||||
'text-red-800': summary.overall_status === 'non_compliant' || summary.overall_status === 'unknown'
|
||||
}">
|
||||
<span x-text="summary.overall_status === 'compliant' ? 'All Checks Passing' :
|
||||
summary.overall_status === 'partial' ? 'Partially Compliant' :
|
||||
'Non-Compliant'"></span>
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
<span x-text="summary.total_passed"></span> of <span x-text="summary.total_checks"></span> checks passing across all templates
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-green-700" x-text="summary.total_passed"></div>
|
||||
<div class="text-xs text-gray-500">Passed</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-red-700" x-text="summary.total_failed"></div>
|
||||
<div class="text-xs text-gray-500">Failed</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-700" x-text="summary.total_checks"></div>
|
||||
<div class="text-xs text-gray-500">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div x-show="alertMessage" x-cloak
|
||||
class="mb-4 rounded-lg p-4 flex items-center gap-3"
|
||||
:class="alertType === 'success' ? 'bg-green-50 border border-green-200 text-green-800' :
|
||||
'bg-red-50 border border-red-200 text-red-800'"
|
||||
role="alert">
|
||||
<i :class="alertType === 'success' ? 'fas fa-check-circle text-green-500' : 'fas fa-exclamation-circle text-red-500'" aria-hidden="true"></i>
|
||||
<span x-text="alertMessage"></span>
|
||||
<button @click="alertMessage = ''" class="ml-auto text-gray-400 hover:text-gray-600" aria-label="Dismiss alert">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Template Cards -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<template x-for="tmpl in templates" :key="tmpl.name">
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<!-- Card Header -->
|
||||
<div class="p-5 border-b"
|
||||
:class="{
|
||||
'border-green-200 bg-green-50': tmpl._status === 'compliant',
|
||||
'border-yellow-200 bg-yellow-50': tmpl._status === 'partial',
|
||||
'border-gray-200': tmpl._status === 'not_applied' || tmpl._status === 'non_compliant'
|
||||
}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-lg font-bold text-gray-800" x-text="tmpl.display_name"></h3>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-green-100 text-green-800': tmpl._status === 'compliant',
|
||||
'bg-yellow-100 text-yellow-800': tmpl._status === 'partial',
|
||||
'bg-red-100 text-red-800': tmpl._status === 'non_compliant',
|
||||
'bg-gray-100 text-gray-600': tmpl._status === 'not_applied'
|
||||
}">
|
||||
<span x-text="tmpl._status === 'compliant' ? 'Compliant' :
|
||||
tmpl._status === 'partial' ? 'Partial' :
|
||||
tmpl._status === 'non_compliant' ? 'Non-Compliant' :
|
||||
'Not Applied'"></span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600" x-text="tmpl.description"></p>
|
||||
</div>
|
||||
|
||||
<!-- Check Results (when expanded) -->
|
||||
<div class="p-5" x-show="tmpl._expanded" x-collapse>
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">
|
||||
<i class="fas fa-clipboard-check mr-1" aria-hidden="true"></i>
|
||||
Compliance Checks (<span x-text="tmpl._statusData ? tmpl._statusData.passed : '?'"></span>/<span x-text="tmpl._statusData ? tmpl._statusData.total : '?'"></span>)
|
||||
</h4>
|
||||
<ul class="space-y-2">
|
||||
<template x-for="check in (tmpl._statusData ? tmpl._statusData.check_results : [])" :key="check.key">
|
||||
<li class="flex items-start gap-2 text-sm">
|
||||
<span class="mt-0.5 flex-shrink-0">
|
||||
<i :class="check.passing ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<span class="font-medium" x-text="check.label"></span>
|
||||
<p class="text-xs text-gray-500" x-text="check.description"></p>
|
||||
<p class="text-xs mt-0.5" x-show="!check.passing">
|
||||
<span class="text-red-600">Current: <code x-text="check.actual"></code></span>
|
||||
<span class="text-gray-400 mx-1">→</span>
|
||||
<span class="text-green-600">Expected: <code x-text="check.expected"></code></span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card Footer -->
|
||||
<div class="px-5 py-4 bg-gray-50 border-t flex items-center justify-between gap-2">
|
||||
<button @click="toggleExpand(tmpl)"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:aria-expanded="tmpl._expanded"
|
||||
:aria-label="'Toggle details for ' + tmpl.display_name">
|
||||
<i class="fas mr-1" :class="tmpl._expanded ? 'fa-chevron-up' : 'fa-chevron-down'" aria-hidden="true"></i>
|
||||
<span x-text="tmpl._expanded ? 'Hide Details' : 'Show Details'"></span>
|
||||
</button>
|
||||
<button @click="applyTemplate(tmpl.name)"
|
||||
:disabled="tmpl._applying"
|
||||
:aria-busy="tmpl._applying"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 min-h-[44px] min-w-[44px]"
|
||||
:class="tmpl._applying ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'"
|
||||
:aria-label="'Apply ' + tmpl.display_name + ' template'"
|
||||
aria-live="polite">>
|
||||
<i class="fas mr-1.5" :class="tmpl._applying ? 'fa-spinner animate-spin' : 'fa-bolt'" aria-hidden="true"></i>
|
||||
<span x-text="tmpl._applying ? 'Applying…' : 'Apply Template'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Applied info -->
|
||||
<div x-show="tmpl.applied_at" class="px-5 py-2 bg-gray-50 border-t text-xs text-gray-500">
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i>
|
||||
Last applied: <span x-text="tmpl.applied_at ? new Date(tmpl.applied_at).toLocaleString() : 'Never'"></span>
|
||||
<span x-show="tmpl.applied_by"> by <span x-text="tmpl.applied_by"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && templates.length === 0" class="text-center py-12 text-gray-500">
|
||||
<i class="fas fa-shield-alt text-4xl mb-4" aria-hidden="true"></i>
|
||||
<p class="text-lg font-medium">No compliance templates available</p>
|
||||
<p class="text-sm">Templates will appear here after seeding.</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div x-show="loading && templates.length === 0" class="text-center py-12">
|
||||
<i class="fas fa-spinner animate-spin text-3xl text-blue-500" aria-hidden="true"></i>
|
||||
<p class="text-gray-500 mt-2">Loading compliance templates…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function complianceApp() {
|
||||
return {
|
||||
templates: [],
|
||||
summary: { overall_status: 'unknown', total_checks: 0, total_passed: 0, total_failed: 0 },
|
||||
loading: true,
|
||||
alertMessage: '',
|
||||
alertType: 'success',
|
||||
|
||||
async init() {
|
||||
await this.refreshAll();
|
||||
},
|
||||
|
||||
async refreshAll() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const [templatesRes, summaryRes] = await Promise.all([
|
||||
fetch('/api/compliance/templates'),
|
||||
fetch('/api/compliance/summary')
|
||||
]);
|
||||
|
||||
if (templatesRes.ok) {
|
||||
const data = await templatesRes.json();
|
||||
this.templates = data.map(t => ({
|
||||
...t,
|
||||
_expanded: false,
|
||||
_applying: false,
|
||||
_status: t.status || 'not_applied',
|
||||
_statusData: null
|
||||
}));
|
||||
|
||||
// Fetch status for each template
|
||||
for (const tmpl of this.templates) {
|
||||
this.fetchTemplateStatus(tmpl);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaryRes.ok) {
|
||||
this.summary = await summaryRes.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load compliance data:', err);
|
||||
this.showAlert('error', 'Failed to load compliance data. Please try again.');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTemplateStatus(tmpl) {
|
||||
try {
|
||||
const res = await fetch(`/api/compliance/templates/${tmpl.name}/status`);
|
||||
if (res.ok) {
|
||||
tmpl._statusData = await res.json();
|
||||
tmpl._status = tmpl._statusData.status;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch status for ${tmpl.name}:`, err);
|
||||
}
|
||||
},
|
||||
|
||||
async toggleExpand(tmpl) {
|
||||
tmpl._expanded = !tmpl._expanded;
|
||||
if (tmpl._expanded && !tmpl._statusData) {
|
||||
await this.fetchTemplateStatus(tmpl);
|
||||
}
|
||||
},
|
||||
|
||||
async applyTemplate(name) {
|
||||
const tmpl = this.templates.find(t => t.name === name);
|
||||
if (!tmpl) return;
|
||||
|
||||
if (!confirm(`Apply the ${tmpl.display_name} compliance template?\n\nThis will update your application settings to meet ${name.toUpperCase()} requirements.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tmpl._applying = true;
|
||||
this.alertMessage = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/compliance/templates/${name}/apply`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.success) {
|
||||
this.showAlert('success', `${tmpl.display_name} template applied successfully.`);
|
||||
await this.refreshAll();
|
||||
} else {
|
||||
this.showAlert('error', data.detail || data.error || 'Failed to apply template.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to apply ${name}:`, err);
|
||||
this.showAlert('error', 'Network error. Please try again.');
|
||||
} finally {
|
||||
tmpl._applying = false;
|
||||
}
|
||||
},
|
||||
|
||||
showAlert(type, message) {
|
||||
this.alertType = type;
|
||||
this.alertMessage = message;
|
||||
if (type === 'success') {
|
||||
setTimeout(() => { this.alertMessage = ''; }, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Credential Audit - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("credentials.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
@@ -8,11 +8,10 @@
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<i class="fas fa-key text-yellow-500 text-2xl" aria-hidden="true"></i>
|
||||
<h1 class="text-3xl font-bold">Credential Audit</h1>
|
||||
<h1 class="text-3xl font-bold">{{ _("credentials.title") }}</h1>
|
||||
</div>
|
||||
<p class="text-gray-600">
|
||||
Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only
|
||||
whether each credential is configured and where it comes from.
|
||||
{{ _("credentials.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +23,7 @@
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-800">{{ total }}</div>
|
||||
<div class="text-sm text-gray-500">Total Credentials</div>
|
||||
<div class="text-sm text-gray-500">{{ _("credentials.total_credentials") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
|
||||
@@ -33,7 +32,7 @@
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-green-700">{{ configured_count }}</div>
|
||||
<div class="text-sm text-gray-500">Configured</div>
|
||||
<div class="text-sm text-gray-500">{{ _("credentials.configured") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
|
||||
@@ -42,32 +41,32 @@
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-600">{{ unconfigured_count }}</div>
|
||||
<div class="text-sm text-gray-500">Not Configured</div>
|
||||
<div class="text-sm text-gray-500">{{ _("credentials.not_configured") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 text-blue-800 p-4 mb-6 rounded-r-md">
|
||||
<p class="font-semibold mb-2"><i class="fas fa-circle-info mr-1" aria-hidden="true"></i> Legend</p>
|
||||
<p class="font-semibold mb-2"><i class="fas fa-circle-info mr-1" aria-hidden="true"></i> {{ _("credentials.legend_title") }}</p>
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
<span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mr-1">DB</span>
|
||||
Value stored in the database (encrypted at rest; overrides environment variable)
|
||||
{{ _("credentials.legend_db") }}
|
||||
</span>
|
||||
<span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-1">ENV</span>
|
||||
Value from environment variable or <code>.env</code> file
|
||||
{{ _("credentials.legend_env_before") }}<code>.env</code>{{ _("credentials.legend_env_after") }}
|
||||
</span>
|
||||
<span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 mr-1">
|
||||
<i class="fas fa-circle-exclamation mr-0.5 text-xs"></i> Missing
|
||||
<i class="fas fa-circle-exclamation mr-0.5 text-xs"></i> {{ _("credentials.status_missing") }}
|
||||
</span>
|
||||
Credential not set — integration will not work
|
||||
{{ _("credentials.legend_missing") }}
|
||||
</span>
|
||||
<span>
|
||||
<span class="text-orange-500 mr-1"><i class="fas fa-rotate"></i></span>
|
||||
Restart required when this credential is rotated
|
||||
{{ _("credentials.legend_restart") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,29 +78,29 @@
|
||||
<div class="bg-gray-100 px-6 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-800">{{ category }}</h2>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ creds | selectattr('configured') | list | length }} / {{ creds | length }} configured
|
||||
{{ creds | selectattr('configured') | list | length }} / {{ creds | length }} {{ _("credentials.configured") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Credentials Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="Credentials for {{ category }}">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="{{ _('credentials.table_for') ~ ' ' ~ category }}">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-56">
|
||||
Credential
|
||||
{{ _("credentials.col_credential") }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
{{ _("common.description") }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
|
||||
Status
|
||||
{{ _("common.status") }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
|
||||
Source
|
||||
{{ _("credentials.col_source") }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
|
||||
Action
|
||||
{{ _("credentials.col_action") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -114,7 +113,7 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<code class="text-sm font-mono text-gray-800">{{ cred.key }}</code>
|
||||
{% if cred.restart_required %}
|
||||
<span title="Restart required after rotating this credential">
|
||||
<span title="{{ _('credentials.restart_title') }}">
|
||||
<i class="fas fa-rotate text-orange-400 text-xs"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -130,11 +129,11 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if cred.configured %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800">
|
||||
<i class="fas fa-check mr-1" aria-hidden="true"></i> Configured
|
||||
<i class="fas fa-check mr-1" aria-hidden="true"></i> {{ _("credentials.configured") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">
|
||||
<i class="fas fa-circle-exclamation mr-1" aria-hidden="true"></i> Missing
|
||||
<i class="fas fa-circle-exclamation mr-1" aria-hidden="true"></i> {{ _("credentials.status_missing") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -142,11 +141,11 @@
|
||||
<!-- Source badge -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if cred.source == 'db' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="Stored in database (encrypted)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="{{ _('credentials.source_db_title') }}">
|
||||
<i class="fas fa-database mr-1 text-xs" aria-hidden="true"></i> DB
|
||||
</span>
|
||||
{% elif cred.source == 'env' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" title="From environment variable">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" title="{{ _('credentials.source_env_title') }}">
|
||||
<i class="fas fa-terminal mr-1 text-xs" aria-hidden="true"></i> ENV
|
||||
</span>
|
||||
{% else %}
|
||||
@@ -157,8 +156,8 @@
|
||||
<!-- Quick-edit link -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<a href="/settings#{{ cred.key }}"
|
||||
title="Edit in Settings"
|
||||
aria-label="Edit {{ cred.key }}"
|
||||
title="{{ _('credentials.edit_in_settings') }}"
|
||||
aria-label="{{ _('common.edit') ~ ' ' ~ cred.key }}"
|
||||
class="inline-flex items-center justify-center w-7 h-7 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<i class="fas fa-pen-to-square text-sm" aria-hidden="true"></i>
|
||||
@@ -178,13 +177,13 @@
|
||||
<a href="/settings"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
<i class="fas fa-cog" aria-hidden="true"></i> Manage Settings
|
||||
<i class="fas fa-cog" aria-hidden="true"></i> {{ _("credentials.manage_settings") }}
|
||||
</a>
|
||||
<a href="/api/settings/credentials"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 text-sm font-medium"
|
||||
>
|
||||
<i class="fas fa-code" aria-hidden="true"></i> Raw JSON (API)
|
||||
<i class="fas fa-code" aria-hidden="true"></i> {{ _("credentials.raw_json") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Duplicate Documents - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("duplicates.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
@@ -88,22 +88,20 @@
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="dup-container px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-2">Duplicate Documents</h1>
|
||||
<h1 class="text-2xl font-bold mb-2">{{ _("duplicates.heading") }}</h1>
|
||||
<p class="text-gray-500 mb-6 text-sm">
|
||||
Documents with identical content (same SHA-256 hash) are shown as
|
||||
<span class="dup-badge dup-badge-exact">exact duplicates</span>.
|
||||
Use the <strong>Near-Duplicate Finder</strong> tab to detect documents with the
|
||||
same scanned content but different hashes — for example, a document scanned
|
||||
twice.
|
||||
{{ _("duplicates.subtitle_1") }}
|
||||
<span class="dup-badge dup-badge-exact">{{ _("duplicates.exact_duplicates_badge") }}</span>.
|
||||
{{ _("duplicates.subtitle_2_pre") }} <strong>{{ _("duplicates.tab_near") }}</strong> {{ _("duplicates.subtitle_2_post") }}
|
||||
</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs" role="tablist" aria-label="Duplicate detection tabs">
|
||||
<div class="tabs" role="tablist" aria-label="{{ _('duplicates.tabs_aria') }}">
|
||||
<button class="tab-btn active" role="tab" aria-selected="true"
|
||||
aria-controls="tab-exact" id="btn-exact"
|
||||
onclick="switchTab('exact')">
|
||||
<i class="fas fa-clone mr-1" aria-hidden="true"></i>
|
||||
Exact Duplicates
|
||||
{{ _("duplicates.tab_exact") }}
|
||||
{% if total_groups is defined %}
|
||||
<span class="ml-1 text-xs bg-red-100 text-red-700 font-bold px-1.5 rounded-full">
|
||||
{{ total_groups }}
|
||||
@@ -114,7 +112,7 @@
|
||||
aria-controls="tab-near" id="btn-near"
|
||||
onclick="switchTab('near')">
|
||||
<i class="fas fa-layer-group mr-1" aria-hidden="true"></i>
|
||||
Near-Duplicate Finder
|
||||
{{ _("duplicates.tab_near") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -123,12 +121,12 @@
|
||||
|
||||
{% if groups %}
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Found <strong>{{ total_groups }}</strong> duplicate group(s) with
|
||||
<strong>{{ total_duplicate_files }}</strong> duplicate file(s) total.
|
||||
{{ _("duplicates.found_prefix") }} <strong>{{ total_groups }}</strong> {{ _("duplicates.found_groups") }}
|
||||
<strong>{{ total_duplicate_files }}</strong> {{ _("duplicates.found_files") }}
|
||||
</p>
|
||||
|
||||
{% for group in groups %}
|
||||
<section class="dup-group" aria-label="Duplicate group {{ loop.index }}">
|
||||
<section class="dup-group" aria-label="{{ _('duplicates.group_aria') }} {{ loop.index }}">
|
||||
<div class="dup-group-header">
|
||||
<i class="fas fa-fingerprint text-gray-400" aria-hidden="true"></i>
|
||||
<span class="text-xs font-mono text-gray-500" title="SHA-256 hash">
|
||||
@@ -136,14 +134,14 @@
|
||||
</span>
|
||||
<span class="dup-badge dup-badge-exact">
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
{{ group.duplicate_count }} duplicate{{ 's' if group.duplicate_count != 1 else '' }}
|
||||
{{ group.duplicate_count }} {{ _("duplicates.dup_singular") if group.duplicate_count == 1 else _("duplicates.dup_plural") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Original file -->
|
||||
{% if group.original %}
|
||||
<div class="dup-file-row original">
|
||||
<span class="dup-role-tag tag-original">Original</span>
|
||||
<span class="dup-role-tag tag-original">{{ _("duplicates.role_original") }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="dup-filename">
|
||||
<a href="/files/{{ group.original.id }}"
|
||||
@@ -164,8 +162,8 @@
|
||||
<div class="dup-actions">
|
||||
<a href="/files/{{ group.original.id }}"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
aria-label="View original file {{ group.original.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> View
|
||||
aria-label="{{ _('duplicates.view_original_aria') }} {{ group.original.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> {{ _("common.view") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +172,7 @@
|
||||
<!-- Duplicate files -->
|
||||
{% for dup in group.duplicates %}
|
||||
<div class="dup-file-row duplicate">
|
||||
<span class="dup-role-tag tag-duplicate">Duplicate</span>
|
||||
<span class="dup-role-tag tag-duplicate">{{ _("duplicates.role_duplicate") }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="dup-filename">
|
||||
<a href="/files/{{ dup.id }}" class="hover:text-blue-600">
|
||||
@@ -194,8 +192,8 @@
|
||||
<div class="dup-actions">
|
||||
<a href="/files/{{ dup.id }}"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
aria-label="View duplicate file {{ dup.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> View
|
||||
aria-label="{{ _('duplicates.view_dup_aria') }} {{ dup.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> {{ _("common.view") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,23 +203,23 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav class="pagination" aria-label="Pagination">
|
||||
<nav class="pagination" aria-label="{{ _('duplicates.pagination_aria') }}">
|
||||
<button class="page-btn" onclick="changePage({{ pagination.page - 1 }})"
|
||||
{% if pagination.page <= 1 %}disabled{% endif %}
|
||||
aria-label="Previous page">
|
||||
aria-label="{{ _('common.prev_page') }}">
|
||||
<i class="fas fa-chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% for p in range(1, pagination.pages + 1) %}
|
||||
<button class="page-btn {% if p == pagination.page %}current{% endif %}"
|
||||
onclick="changePage({{ p }})"
|
||||
aria-label="Page {{ p }}"
|
||||
aria-label="{{ _('common.page') }} {{ p }}"
|
||||
{% if p == pagination.page %}aria-current="page"{% endif %}>
|
||||
{{ p }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
<button class="page-btn" onclick="changePage({{ pagination.page + 1 }})"
|
||||
{% if pagination.page >= pagination.pages %}disabled{% endif %}
|
||||
aria-label="Next page">
|
||||
aria-label="{{ _('common.next_page') }}">
|
||||
<i class="fas fa-chevron-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -230,11 +228,9 @@
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-check-circle text-green-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700">No exact duplicates found</p>
|
||||
<p class="font-semibold text-lg text-gray-700">{{ _("duplicates.no_exact_heading") }}</p>
|
||||
<p class="text-sm mt-1">
|
||||
Every file in the system has a unique SHA-256 hash.
|
||||
Use the <strong>Near-Duplicate Finder</strong> tab to check for
|
||||
re-scanned documents.
|
||||
{{ _("duplicates.no_exact_detail_pre") }} <strong>{{ _("duplicates.tab_near") }}</strong> {{ _("duplicates.no_exact_detail_post") }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -246,32 +242,29 @@
|
||||
<div class="near-dup-search">
|
||||
<h3>
|
||||
<i class="fas fa-search mr-1 text-yellow-500" aria-hidden="true"></i>
|
||||
Find Near-Duplicates for a Document
|
||||
{{ _("duplicates.near_dup_heading") }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Select a document to check whether any other files contain the same (or very
|
||||
similar) content — even if they were scanned at different times and have
|
||||
different SHA-256 hashes. Similarity is computed from OCR text embeddings;
|
||||
documents without extracted text cannot be compared.
|
||||
{{ _("duplicates.near_dup_desc") }}
|
||||
</p>
|
||||
|
||||
<form id="nearDupForm" onsubmit="findNearDups(event)">
|
||||
<div class="nd-form">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndFileId">File ID</label>
|
||||
<label for="ndFileId">{{ _("duplicates.file_id_label") }}</label>
|
||||
<input type="number" id="ndFileId" name="file_id" min="1"
|
||||
placeholder="e.g. 42" required
|
||||
placeholder="{{ _('duplicates.file_id_placeholder') }}" required
|
||||
style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndThreshold">Similarity threshold</label>
|
||||
<label for="ndThreshold">{{ _("duplicates.threshold_label") }}</label>
|
||||
<input type="number" id="ndThreshold" name="threshold"
|
||||
min="0" max="1" step="0.05"
|
||||
value="{{ near_duplicate_threshold }}"
|
||||
style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndLimit">Max results</label>
|
||||
<label for="ndLimit">{{ _("duplicates.max_results_label") }}</label>
|
||||
<select id="ndLimit" name="limit" style="min-height:44px;">
|
||||
<option value="5" selected>5</option>
|
||||
<option value="10">10</option>
|
||||
@@ -282,7 +275,7 @@
|
||||
class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold
|
||||
px-5 rounded-lg transition-colors"
|
||||
style="min-height:44px;">
|
||||
<i class="fas fa-search mr-1" aria-hidden="true"></i> Find
|
||||
<i class="fas fa-search mr-1" aria-hidden="true"></i> {{ _("duplicates.find_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -296,20 +289,14 @@
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
|
||||
<p class="font-semibold mb-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
How near-duplicate detection works
|
||||
{{ _("duplicates.how_it_works_heading") }}
|
||||
</p>
|
||||
<ul class="list-disc ml-5 space-y-1">
|
||||
<li>After OCR processes a document, its text is converted to a numeric
|
||||
<em>embedding vector</em> using an AI language model.</li>
|
||||
<li>Near-duplicate detection compares these vectors using <em>cosine
|
||||
similarity</em>: a score of 1.0 means identical content, 0.0 means
|
||||
completely different.</li>
|
||||
<li>A threshold of <strong>{{ near_duplicate_threshold }}</strong> means
|
||||
documents must share at least
|
||||
{{ (near_duplicate_threshold * 100)|round|int }}% semantic similarity
|
||||
to be flagged.</li>
|
||||
<li>Documents scanned twice (even with slight differences) will typically
|
||||
score <strong>> 0.90</strong>.</li>
|
||||
<li>{{ _("duplicates.how_step1_pre") }} <em>{{ _("duplicates.embedding_vector") }}</em> {{ _("duplicates.how_step1_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step2_pre") }} <em>{{ _("duplicates.cosine_similarity") }}</em>{{ _("duplicates.how_step2_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step3_pre") }} <strong>{{ near_duplicate_threshold }}</strong> {{ _("duplicates.how_step3_mid") }}
|
||||
{{ (near_duplicate_threshold * 100)|round|int }}% {{ _("duplicates.how_step3_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step4") }} <strong>> 0.90</strong>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div><!-- /tab-near -->
|
||||
|
||||
@@ -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>
|
||||
@@ -525,32 +524,33 @@
|
||||
<option value="webdav" {% if storage_provider == "webdav" %}selected{% endif %}>WebDAV</option>
|
||||
<option value="ftp" {% if storage_provider == "ftp" %}selected{% endif %}>FTP</option>
|
||||
<option value="sftp" {% if storage_provider == "sftp" %}selected{% endif %}>SFTP</option>
|
||||
<option value="icloud" {% if storage_provider == "icloud" %}selected{% endif %}>iCloud Drive</option>
|
||||
</select>
|
||||
</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 +564,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 +579,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 +594,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 +620,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 +644,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 +661,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 +671,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 +681,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 +690,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 +701,7 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
</th>
|
||||
<th scope="col">Actions</th>
|
||||
<th scope="col">{{ _("files.table_actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -736,7 +736,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 +778,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 +813,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 +826,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>
|
||||
|
||||
+84
-102
@@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Help Center – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("help.page_title") }} – DocuElevate{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<!-- SEO meta tags -->
|
||||
<meta name="description" content="DocuElevate Help Center – learn how to upload, process, and route your documents automatically. Get started with sources, destinations, workflows, and integrations.">
|
||||
<meta name="description" content="{{ _('help.meta_description') }}">
|
||||
<meta name="keywords" content="DocuElevate, help, support, document processing, OCR, cloud storage, workflow automation, SaaS">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://{{ external_hostname }}/help">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Help Center – DocuElevate">
|
||||
<meta property="og:description" content="Everything you need to get started with DocuElevate: upload documents, automate workflows, and connect your favourite cloud storage.">
|
||||
<meta property="og:title" content="{{ _('help.page_title') }} – DocuElevate">
|
||||
<meta property="og:description" content="{{ _('help.og_description') }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://{{ external_hostname }}/help">
|
||||
|
||||
@@ -63,18 +63,17 @@
|
||||
<!-- ═══════════════════════ Hero Section ═══════════════════════ -->
|
||||
<header class="text-center mb-14">
|
||||
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 mb-4">
|
||||
<i class="fas fa-life-ring text-blue-500 mr-2" aria-hidden="true"></i>Help Center
|
||||
<i class="fas fa-life-ring text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.heading") }}
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed">
|
||||
Everything you need to get the most out of DocuElevate.
|
||||
Browse topics below or search for what you need.
|
||||
{{ _("help.subheading") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════════════════ Quick-start Cards ═══════════════════════ -->
|
||||
<section aria-labelledby="quickstart-heading" class="mb-16">
|
||||
<h2 id="quickstart-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-rocket text-blue-500 mr-2" aria-hidden="true"></i>Quick Start
|
||||
<i class="fas fa-rocket text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.quickstart_heading") }}
|
||||
</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
@@ -84,11 +83,10 @@
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 text-blue-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-cloud-arrow-up"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Upload Documents</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-800">{{ _("help.quickstart_upload") }}</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Drag & drop files onto the <strong>Upload</strong> page or click <em>Choose File</em>.
|
||||
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
|
||||
{{ _("help.quickstart_upload_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -98,11 +96,10 @@
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-green-100 text-green-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-plug"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Connect Storage</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-800">{{ _("help.quickstart_storage") }}</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Head to <strong>Settings → Integrations</strong> and link your cloud accounts.
|
||||
Processed documents are automatically routed to every destination you configure.
|
||||
{{ _("help.quickstart_storage_desc_full") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -112,11 +109,10 @@
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-purple-100 text-purple-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-wand-magic-sparkles"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Automate Workflows</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-800">{{ _("help.quickstart_workflows") }}</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Create <strong>Pipelines</strong> to define multi-step processing and routing rules.
|
||||
Combine OCR, AI extraction, format conversion, and delivery in a single flow.
|
||||
{{ _("help.quickstart_workflows_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -126,39 +122,35 @@
|
||||
<!-- ═══════════════════════ Sources ═══════════════════════ -->
|
||||
<section aria-labelledby="sources-heading" class="mb-16">
|
||||
<h2 id="sources-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-arrow-right-to-bracket text-blue-500 mr-2" aria-hidden="true"></i>Sources – Getting Documents In
|
||||
<i class="fas fa-arrow-right-to-bracket text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.sources_heading") }}
|
||||
</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-upload mr-2 text-gray-400" aria-hidden="true"></i>Web Upload</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-upload mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.sources_web_upload") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
The fastest way to get started. Open the <strong>Upload</strong> page, drop one or more files, and DocuElevate takes care of the rest.
|
||||
Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.
|
||||
{{ _("help.sources_web_upload_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-envelope-open-text mr-2 text-gray-400" aria-hidden="true"></i>Email Ingestion (IMAP)</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-envelope-open-text mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.sources_email_ingestion") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Forward documents to a dedicated mailbox. Under <strong>Email Ingestion</strong>, add one or more IMAP accounts.
|
||||
DocuElevate polls for new messages and processes attachments automatically.
|
||||
{{ _("help.sources_email_ingestion_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-code mr-2 text-gray-400" aria-hidden="true"></i>REST API</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-code mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.sources_rest_api") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Integrate programmatically by <code class="bg-gray-100 px-1 rounded text-xs">POST</code>-ing files to <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
|
||||
Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.
|
||||
{{ _("help.sources_rest_api_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Scanner & Mobile</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.sources_scanner") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Point network scanners (HP, Fujitsu ScanSnap, Brother) at DocuElevate's upload endpoint or
|
||||
use any mobile scanning app that supports custom HTTP destinations.
|
||||
{{ _("help.sources_scanner_desc_full") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -168,79 +160,79 @@
|
||||
<!-- ═══════════════════════ Destinations ═══════════════════════ -->
|
||||
<section aria-labelledby="destinations-heading" class="mb-16">
|
||||
<h2 id="destinations-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-arrow-right-from-bracket text-blue-500 mr-2" aria-hidden="true"></i>Destinations – Where Documents Go
|
||||
<i class="fas fa-arrow-right-from-bracket text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.destinations_heading") }}
|
||||
</h2>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-dropbox text-blue-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Dropbox</h3>
|
||||
<p class="text-gray-500 text-xs">OAuth-linked. Files land in your chosen folder.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_dropbox") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_dropbox_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-google-drive text-green-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Google Drive</h3>
|
||||
<p class="text-gray-500 text-xs">Service account or OAuth. Supports shared drives.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_google_drive") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_google_drive_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-microsoft text-blue-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">OneDrive</h3>
|
||||
<p class="text-gray-500 text-xs">Microsoft Graph API integration.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_onedrive") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_onedrive_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-aws text-yellow-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Amazon S3</h3>
|
||||
<p class="text-gray-500 text-xs">Any S3-compatible bucket (AWS, MinIO, Wasabi).</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_s3") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_s3_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-cloud text-blue-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Nextcloud / WebDAV</h3>
|
||||
<p class="text-gray-500 text-xs">Self-hosted cloud storage via WebDAV.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_nextcloud") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_nextcloud_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-leaf text-green-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Paperless-ngx</h3>
|
||||
<p class="text-gray-500 text-xs">Push documents straight into Paperless for archival.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_paperless") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_paperless_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-server text-gray-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">SFTP / FTP</h3>
|
||||
<p class="text-gray-500 text-xs">Secure file transfer to any server.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_sftp") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_sftp_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-envelope text-red-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Email Forwarding</h3>
|
||||
<p class="text-gray-500 text-xs">Processed files sent as SMTP attachments.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_email") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_email_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-link text-indigo-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Webhook</h3>
|
||||
<p class="text-gray-500 text-xs">POST metadata to any external endpoint.</p>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">{{ _("help.destinations_webhook") }}</h3>
|
||||
<p class="text-gray-500 text-xs">{{ _("help.destinations_webhook_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -250,32 +242,31 @@
|
||||
<!-- ═══════════════════════ Workflows / Pipelines ═══════════════════════ -->
|
||||
<section aria-labelledby="workflows-heading" class="mb-16">
|
||||
<h2 id="workflows-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-diagram-project text-blue-500 mr-2" aria-hidden="true"></i>Workflows & Pipelines
|
||||
<i class="fas fa-diagram-project text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.workflows_heading") }}
|
||||
</h2>
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">What is a Pipeline?</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">{{ _("help.workflows_what_is") }}</h3>
|
||||
<p class="text-gray-600 text-sm leading-relaxed mb-4">
|
||||
A <strong>Pipeline</strong> is a series of processing steps that run automatically whenever a document is ingested.
|
||||
Each step can transform, enrich, or route the document.
|
||||
{{ _("help.workflows_definition") }}
|
||||
</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">Typical Steps</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">{{ _("help.workflows_typical_steps") }}</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-1">
|
||||
<li>Convert to PDF (Gotenberg)</li>
|
||||
<li>OCR – extract text (Azure, Tesseract, Mistral …)</li>
|
||||
<li>AI metadata extraction (OpenAI, Claude, Gemini …)</li>
|
||||
<li>Deliver to one or more destinations</li>
|
||||
<li>{{ _("help.workflows_step_1_full") }}</li>
|
||||
<li>{{ _("help.workflows_step_2_full") }}</li>
|
||||
<li>{{ _("help.workflows_step_3_full") }}</li>
|
||||
<li>{{ _("help.workflows_step_4") }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">Creating a Pipeline</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">{{ _("help.workflows_creating") }}</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-2 leading-relaxed">
|
||||
<li>Go to <strong>Pipelines</strong> in the main menu.</li>
|
||||
<li>Click <em>New Pipeline</em> and give it a name.</li>
|
||||
<li>Add the processing steps you need.</li>
|
||||
<li>Choose one or more delivery destinations.</li>
|
||||
<li>Save – new documents will be processed through this pipeline automatically.</li>
|
||||
<li>{{ _("help.workflows_step_1_create") }}</li>
|
||||
<li>{{ _("help.workflows_step_2_create") }}</li>
|
||||
<li>{{ _("help.workflows_step_3_create") }}</li>
|
||||
<li>{{ _("help.workflows_step_4_create") }}</li>
|
||||
<li>{{ _("help.workflows_step_5_create") }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,47 +276,43 @@
|
||||
<!-- ═══════════════════════ Data Ingestion Tools ═══════════════════════ -->
|
||||
<section aria-labelledby="ingestion-heading" class="mb-16">
|
||||
<h2 id="ingestion-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-toolbox text-blue-500 mr-2" aria-hidden="true"></i>Ingesting Data with Standard Tools
|
||||
<i class="fas fa-toolbox text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.ingestion_heading") }}
|
||||
</h2>
|
||||
<div class="bg-white shadow rounded-lg p-6 space-y-6">
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-terminal mr-2 text-gray-400" aria-hidden="true"></i>cURL</h3>
|
||||
<p class="text-gray-600 text-sm mb-2">Upload a file from any terminal:</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-terminal mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.ingestion_curl") }}</h3>
|
||||
<p class="text-gray-600 text-sm mb-2">{{ _("help.ingestion_curl_desc") }}</p>
|
||||
<pre class="bg-gray-50 text-sm rounded p-3 overflow-x-auto"><code>curl -X POST https://your-instance/api/upload \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-F "file=@/path/to/document.pdf"</code></pre>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>Watched Folders</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.ingestion_watched_folders") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Use <strong>inotifywait</strong> (Linux), <strong>fswatch</strong> (macOS), or any file-watcher script to
|
||||
<code class="bg-gray-100 px-1 rounded text-xs">POST</code> new files as soon as they appear in a local directory.
|
||||
{{ _("help.ingestion_watched_folders_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Network Scanners</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.ingestion_scanners") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Configure your HP, Fujitsu ScanSnap, or Brother scanner to use DocuElevate's upload URL as a
|
||||
<em>Scan-to-Network</em> destination. Scans are processed the moment they arrive.
|
||||
{{ _("help.ingestion_scanners_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-mobile-screen-button mr-2 text-gray-400" aria-hidden="true"></i>Mobile Scanning Apps</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-mobile-screen-button mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.ingestion_mobile") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Apps like <strong>Adobe Scan</strong>, <strong>Microsoft Lens</strong>, or <strong>Genius Scan</strong>
|
||||
can export to a custom HTTP endpoint. Point them at <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
|
||||
{{ _("help.ingestion_mobile_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-shuffle mr-2 text-gray-400" aria-hidden="true"></i>Zapier / n8n / Make</h3>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-shuffle mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.ingestion_zapier") }}</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Connect DocuElevate to thousands of other apps via integration platforms.
|
||||
Use the HTTP/Webhook module to <code class="bg-gray-100 px-1 rounded text-xs">POST</code> files to the upload API.
|
||||
{{ _("help.ingestion_zapier_desc") }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -335,7 +322,7 @@
|
||||
<!-- ═══════════════════════ FAQ ═══════════════════════ -->
|
||||
<section aria-labelledby="faq-heading" class="mb-16">
|
||||
<h2 id="faq-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-circle-question text-blue-500 mr-2" aria-hidden="true"></i>Frequently Asked Questions
|
||||
<i class="fas fa-circle-question text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.faq_heading") }}
|
||||
</h2>
|
||||
<div class="space-y-4" x-data="{ open: null }">
|
||||
|
||||
@@ -345,12 +332,11 @@
|
||||
@click="open = open === 1 ? null : 1"
|
||||
:aria-expanded="open === 1 ? 'true' : 'false'"
|
||||
aria-controls="faq-1">
|
||||
<span>How do I upload documents?</span>
|
||||
<span>{{ _("help.faq_1_q") }}</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 1 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-1" x-show="open === 1" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Navigate to the <strong>Upload</strong> page, drag-and-drop your files or click <em>Choose File</em>.
|
||||
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
|
||||
{{ _("help.faq_1_a") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -360,12 +346,11 @@
|
||||
@click="open = open === 2 ? null : 2"
|
||||
:aria-expanded="open === 2 ? 'true' : 'false'"
|
||||
aria-controls="faq-2">
|
||||
<span>Which file formats are supported?</span>
|
||||
<span>{{ _("help.faq_2_q") }}</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 2 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-2" x-show="open === 2" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML.
|
||||
Non-PDF files are automatically converted to PDF before processing.
|
||||
{{ _("help.faq_2_a") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -375,12 +360,11 @@
|
||||
@click="open = open === 3 ? null : 3"
|
||||
:aria-expanded="open === 3 ? 'true' : 'false'"
|
||||
aria-controls="faq-3">
|
||||
<span>Can I ingest documents from email?</span>
|
||||
<span>{{ _("help.faq_3_q") }}</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 3 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-3" x-show="open === 3" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Yes. Go to <strong>Email Ingestion</strong>, add an IMAP account, and DocuElevate will poll for new
|
||||
messages and process attachments automatically.
|
||||
{{ _("help.faq_3_a") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,12 +374,11 @@
|
||||
@click="open = open === 4 ? null : 4"
|
||||
:aria-expanded="open === 4 ? 'true' : 'false'"
|
||||
aria-controls="faq-4">
|
||||
<span>How do processing pipelines work?</span>
|
||||
<span>{{ _("help.faq_4_q") }}</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 4 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-4" x-show="open === 4" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the
|
||||
result to one or more destinations. Create and manage them from the <strong>Pipelines</strong> page.
|
||||
{{ _("help.faq_4_a") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -405,12 +388,11 @@
|
||||
@click="open = open === 5 ? null : 5"
|
||||
:aria-expanded="open === 5 ? 'true' : 'false'"
|
||||
aria-controls="faq-5">
|
||||
<span>Is my data secure?</span>
|
||||
<span>{{ _("help.faq_5_q") }}</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 5 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-5" x-show="open === 5" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents
|
||||
longer than necessary. See the <a href="/privacy" class="text-blue-600 hover:underline">Privacy Notice</a> for full details.
|
||||
{{ _("help.faq_5_a_pre") }}<a href="/privacy" class="text-blue-600 hover:underline">{{ _("help.privacy_notice") }}</a>{{ _("help.faq_5_a_post") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -420,7 +402,7 @@
|
||||
<!-- ═══════════════════════ Contact / Support ═══════════════════════ -->
|
||||
<section aria-labelledby="support-heading" class="mb-10">
|
||||
<h2 id="support-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-headset text-blue-500 mr-2" aria-hidden="true"></i>Contact Support
|
||||
<i class="fas fa-headset text-blue-500 mr-2" aria-hidden="true"></i>{{ _("help.support_heading") }}
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
@@ -428,7 +410,7 @@
|
||||
<!-- Left column: info -->
|
||||
<div class="bg-white shadow rounded-lg p-6 space-y-4">
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Can't find what you're looking for? Our support team is here to help.
|
||||
{{ _("help.support_description") }}
|
||||
</p>
|
||||
|
||||
{% if support_email %}
|
||||
@@ -441,7 +423,7 @@
|
||||
{% if zammad_chat_enabled and zammad_url %}
|
||||
<div class="flex items-center text-sm text-gray-700">
|
||||
<i class="fas fa-comments text-blue-500 mr-2" aria-hidden="true"></i>
|
||||
<span>Live chat available – look for the chat bubble in the bottom-right corner.</span>
|
||||
<span>{{ _("help.support_live_chat") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -450,22 +432,22 @@
|
||||
{% if zammad_form_enabled and zammad_url %}
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-3">
|
||||
<i class="fas fa-ticket mr-2 text-gray-400" aria-hidden="true"></i>Open a Support Ticket
|
||||
<i class="fas fa-ticket mr-2 text-gray-400" aria-hidden="true"></i>{{ _("help.support_ticket_heading") }}
|
||||
</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Describe your issue below and we'll get back to you as soon as possible.
|
||||
{{ _("help.support_ticket_desc") }}
|
||||
</p>
|
||||
<button id="zammad-feedback-form"
|
||||
type="button"
|
||||
class="inline-flex items-center bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-5 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 min-h-[44px]">
|
||||
<i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>Submit a Ticket
|
||||
<i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>{{ _("help.support_ticket_button") }}
|
||||
</button>
|
||||
</div>
|
||||
{% elif not zammad_form_enabled and not support_email and not zammad_chat_enabled %}
|
||||
<div class="bg-white shadow rounded-lg p-6 flex flex-col items-center justify-center text-center">
|
||||
<i class="fas fa-info-circle text-blue-400 text-2xl mb-2" aria-hidden="true"></i>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Contact your administrator for support information.
|
||||
{{ _("help.support_admin_message") }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -172,6 +172,11 @@
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Delete after process
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="acct.profile_id">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-blue-50 text-blue-700">
|
||||
<i class="fas fa-filter mr-1" aria-hidden="true"></i><span x-text="profileName(acct.profile_id)"></span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="acct.last_checked_at">
|
||||
<span class="text-gray-400" :title="acct.last_checked_at">
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i>Last polled: <span x-text="formatDate(acct.last_checked_at)"></span>
|
||||
@@ -386,6 +391,49 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Attachment filter -->
|
||||
<div>
|
||||
<label for="acct-profile" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
||||
Ingestion Profile
|
||||
</label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<select
|
||||
id="acct-profile"
|
||||
x-model.number="form.profile_id"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
aria-describedby="acct-profile-hint"
|
||||
>
|
||||
<option :value="null">Use global default</option>
|
||||
<template x-for="profile in profiles" :key="profile.id">
|
||||
<option :value="profile.id" x-text="profile.name + (profile.is_builtin ? '' : ' (custom)')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
@click="openProfileModal(null)"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:40px;"
|
||||
title="Create new profile"
|
||||
aria-label="Create new ingestion profile"
|
||||
>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p id="acct-profile-hint" class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
Controls which attachment types are ingested. Leave blank to use the global default (documents only).
|
||||
<button type="button" @click="showProfilesSection = !showProfilesSection" class="underline hover:text-blue-600 focus:outline-none">
|
||||
Manage profiles
|
||||
</button>
|
||||
</p>
|
||||
<!-- Inline selected profile summary -->
|
||||
<template x-if="form.profile_id">
|
||||
<div class="mt-2 text-xs bg-blue-50 dark:bg-blue-900/20 rounded px-2 py-1.5 text-blue-800 dark:text-blue-200">
|
||||
<strong x-text="profileName(form.profile_id)"></strong>:
|
||||
<span x-text="profileCategorySummary(form.profile_id)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Test connection result -->
|
||||
<template x-if="testResult">
|
||||
<div
|
||||
@@ -494,10 +542,277 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Ingestion Profiles panel ─────────────────────────────────────────────── -->
|
||||
<div
|
||||
x-show="showProfilesSection"
|
||||
x-transition
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="profiles-panel-title"
|
||||
@keydown.escape.window="showProfilesSection = false"
|
||||
style="display:none;"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl overflow-y-auto max-h-[90vh]" @click.stop>
|
||||
<div class="px-6 pt-6 pb-2 flex items-center justify-between">
|
||||
<h2 id="profiles-panel-title" class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<i class="fas fa-filter text-blue-500" aria-hidden="true"></i>
|
||||
Ingestion Profiles
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
@click="showProfilesSection = false"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
|
||||
aria-label="Close profiles panel"
|
||||
>
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 pb-6 pt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
Profiles define which attachment types are ingested from emails. Assign a profile to each IMAP account for fine-grained control.
|
||||
</p>
|
||||
|
||||
<!-- Profile list -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<template x-for="profile in profiles" :key="profile.id">
|
||||
<div class="border border-gray-100 dark:border-gray-700 rounded-lg p-4 flex items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium text-sm text-gray-900 dark:text-white" x-text="profile.name"></span>
|
||||
<template x-if="profile.is_builtin">
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500">built-in</span>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5" x-text="profile.description || ''"></p>
|
||||
<!-- Category badges -->
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
<template x-for="cat in profile.allowed_categories" :key="cat">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">
|
||||
<span x-text="categoryLabel(cat)"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@click="openProfileModal(profile)"
|
||||
:disabled="profile.is_builtin"
|
||||
:class="profile.is_builtin ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
class="p-1.5 text-gray-400 hover:text-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
|
||||
:aria-label="`Edit profile ${profile.name}`"
|
||||
:title="profile.is_builtin ? 'Built-in profiles cannot be edited' : 'Edit profile'"
|
||||
>
|
||||
<i class="fas fa-edit text-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="confirmDeleteProfile(profile)"
|
||||
:disabled="profile.is_builtin"
|
||||
:class="profile.is_builtin ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
class="p-1.5 text-gray-400 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 rounded"
|
||||
:aria-label="`Delete profile ${profile.name}`"
|
||||
:title="profile.is_builtin ? 'Built-in profiles cannot be deleted' : 'Delete profile'"
|
||||
>
|
||||
<i class="fas fa-trash-alt text-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="openProfileModal(null)"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded bg-blue-600 hover:bg-blue-700 text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
style="min-height:40px;"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i> New Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Profile create / edit modal ────────────────────────────────────────────── -->
|
||||
<div
|
||||
x-show="profileModalOpen"
|
||||
x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 px-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-labelledby="editingProfile ? 'profile-modal-title-edit' : 'profile-modal-title-create'"
|
||||
@keydown.escape.window="closeProfileModal()"
|
||||
style="display:none;"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg overflow-y-auto max-h-[90vh]" @click.stop>
|
||||
<div class="px-6 pt-6 pb-2 flex items-center justify-between">
|
||||
<h2
|
||||
:id="editingProfile ? 'profile-modal-title-edit' : 'profile-modal-title-create'"
|
||||
class="text-lg font-semibold text-gray-900 dark:text-white"
|
||||
x-text="editingProfile ? 'Edit Profile' : 'New Ingestion Profile'"
|
||||
></h2>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeProfileModal()"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveProfile()" class="px-6 pb-6 pt-4 space-y-4">
|
||||
|
||||
<!-- Profile name -->
|
||||
<div>
|
||||
<label for="profile-name" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
||||
Profile Name <span class="text-red-500" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="profile-name"
|
||||
type="text"
|
||||
x-model="profileForm.name"
|
||||
placeholder="e.g. Scanner Inbox, Finance Documents"
|
||||
required
|
||||
maxlength="255"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="profile-description" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
id="profile-description"
|
||||
type="text"
|
||||
x-model="profileForm.description"
|
||||
placeholder="Optional description"
|
||||
maxlength="500"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Category checkboxes -->
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
File Type Categories <span class="text-red-500" aria-hidden="true">*</span>
|
||||
</legend>
|
||||
<div class="space-y-2">
|
||||
<template x-for="cat in categories" :key="cat.key">
|
||||
<label class="flex items-start gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="cat.key"
|
||||
:checked="profileForm.allowed_categories.includes(cat.key)"
|
||||
@change="toggleCategory(cat.key)"
|
||||
class="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="flex-1">
|
||||
<span class="text-sm font-medium text-gray-800 dark:text-gray-100" x-text="cat.label"></span>
|
||||
<span class="block text-xs text-gray-500 dark:text-gray-400" x-text="cat.description"></span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p x-show="profileForm.allowed_categories.length === 0" class="mt-2 text-xs text-red-600" role="alert">
|
||||
Select at least one category.
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Form error -->
|
||||
<template x-if="profileFormError">
|
||||
<p class="text-sm text-red-600" role="alert" x-text="profileFormError"></p>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-2 pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeProfileModal()"
|
||||
class="px-4 py-2 text-sm font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:40px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="savingProfile || profileForm.allowed_categories.length === 0"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded bg-blue-600 hover:bg-blue-700 text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
style="min-height:40px;"
|
||||
>
|
||||
<template x-if="savingProfile">
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>
|
||||
</template>
|
||||
<span x-text="editingProfile ? 'Save Changes' : 'Create Profile'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Delete profile confirmation modal ────────────────────────────────────── -->
|
||||
<div
|
||||
x-show="deleteProfileModalOpen"
|
||||
x-transition
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 px-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="delete-profile-modal-title"
|
||||
@keydown.escape.window="deleteProfileModalOpen = false"
|
||||
style="display:none;"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-sm p-6" @click.stop>
|
||||
<h2 id="delete-profile-modal-title" class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Delete Profile?
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
Are you sure you want to delete the profile
|
||||
<strong x-text="profileToDelete ? profileToDelete.name : ''"></strong>?
|
||||
IMAP accounts using this profile will fall back to the global default.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="deleteProfileModalOpen = false; profileToDelete = null"
|
||||
class="px-4 py-2 text-sm font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
style="min-height:40px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="deleteProfile()"
|
||||
:disabled="deletingProfile"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded bg-red-600 hover:bg-red-700 text-white focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-50"
|
||||
style="min-height:40px;"
|
||||
>
|
||||
<template x-if="deletingProfile">
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>
|
||||
</template>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function imapAccountsApp() {
|
||||
return {
|
||||
accounts: {{ accounts | tojson }},
|
||||
profiles: {{ profiles | tojson }},
|
||||
categories: {{ categories | tojson }},
|
||||
defaultCategories: {{ default_categories | tojson }},
|
||||
quota: {
|
||||
current_count: {{ current_count }},
|
||||
max_mailboxes: {{ max_mailboxes | tojson }},
|
||||
@@ -508,17 +823,17 @@ function imapAccountsApp() {
|
||||
loading: false,
|
||||
alert: { show: false, type: '', title: '', message: '' },
|
||||
|
||||
// Modal state
|
||||
// Account modal state
|
||||
modalOpen: false,
|
||||
editingAccount: null,
|
||||
form: { name: '', host: '', port: 993, username: '', password: '', use_ssl: true, delete_after_process: false, is_active: true },
|
||||
form: { name: '', host: '', port: 993, username: '', password: '', use_ssl: true, delete_after_process: false, is_active: true, profile_id: null },
|
||||
showPassword: false,
|
||||
saving: false,
|
||||
testing: false,
|
||||
testResult: null,
|
||||
formError: null,
|
||||
|
||||
// Delete state
|
||||
// Account delete state
|
||||
deleteModalOpen: false,
|
||||
accountToDelete: null,
|
||||
deleting: false,
|
||||
@@ -526,6 +841,17 @@ function imapAccountsApp() {
|
||||
// Test (saved account) state
|
||||
testingId: null,
|
||||
|
||||
// Profiles panel & modal state
|
||||
showProfilesSection: false,
|
||||
profileModalOpen: false,
|
||||
editingProfile: null,
|
||||
profileForm: { name: '', description: '', allowed_categories: [] },
|
||||
savingProfile: false,
|
||||
profileFormError: null,
|
||||
deleteProfileModalOpen: false,
|
||||
profileToDelete: null,
|
||||
deletingProfile: false,
|
||||
|
||||
get canAdd() {
|
||||
return this.quota.can_add;
|
||||
},
|
||||
@@ -549,9 +875,33 @@ function imapAccountsApp() {
|
||||
setTimeout(() => { this.alert.show = false; }, 5000);
|
||||
},
|
||||
|
||||
// ── Profile helpers ──────────────────────────────────────────────────────
|
||||
|
||||
profileById(id) {
|
||||
return this.profiles.find(p => p.id === id) || null;
|
||||
},
|
||||
|
||||
profileName(id) {
|
||||
const p = this.profileById(id);
|
||||
return p ? p.name : 'Unknown profile';
|
||||
},
|
||||
|
||||
profileCategorySummary(id) {
|
||||
const p = this.profileById(id);
|
||||
if (!p) return '';
|
||||
return p.allowed_categories.map(k => this.categoryLabel(k)).join(', ');
|
||||
},
|
||||
|
||||
categoryLabel(key) {
|
||||
const cat = this.categories.find(c => c.key === key);
|
||||
return cat ? cat.label : key;
|
||||
},
|
||||
|
||||
// ── Account modal ────────────────────────────────────────────────────────
|
||||
|
||||
openCreateModal() {
|
||||
this.editingAccount = null;
|
||||
this.form = { name: '', host: '', port: 993, username: '', password: '', use_ssl: true, delete_after_process: false, is_active: true };
|
||||
this.form = { name: '', host: '', port: 993, username: '', password: '', use_ssl: true, delete_after_process: false, is_active: true, profile_id: null };
|
||||
this.showPassword = false;
|
||||
this.testResult = null;
|
||||
this.formError = null;
|
||||
@@ -569,6 +919,7 @@ function imapAccountsApp() {
|
||||
use_ssl: acct.use_ssl,
|
||||
delete_after_process: acct.delete_after_process,
|
||||
is_active: acct.is_active,
|
||||
profile_id: acct.profile_id || null,
|
||||
};
|
||||
this.showPassword = false;
|
||||
this.testResult = null;
|
||||
@@ -592,6 +943,8 @@ function imapAccountsApp() {
|
||||
if (this.editingAccount && payload.password === '') {
|
||||
delete payload.password;
|
||||
}
|
||||
// Normalise profile_id: empty string → null
|
||||
if (payload.profile_id === '' || payload.profile_id === 0) payload.profile_id = null;
|
||||
let resp;
|
||||
if (this.editingAccount) {
|
||||
resp = await fetch(`/api/imap-accounts/${this.editingAccount.id}`, {
|
||||
@@ -719,6 +1072,126 @@ function imapAccountsApp() {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Profile CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
openProfileModal(profile) {
|
||||
this.editingProfile = profile;
|
||||
if (profile) {
|
||||
this.profileForm = {
|
||||
name: profile.name,
|
||||
description: profile.description || '',
|
||||
allowed_categories: [...profile.allowed_categories],
|
||||
};
|
||||
} else {
|
||||
this.profileForm = { name: '', description: '', allowed_categories: [...this.defaultCategories] };
|
||||
}
|
||||
this.profileFormError = null;
|
||||
this.profileModalOpen = true;
|
||||
},
|
||||
|
||||
closeProfileModal() {
|
||||
this.profileModalOpen = false;
|
||||
this.editingProfile = null;
|
||||
this.profileFormError = null;
|
||||
},
|
||||
|
||||
toggleCategory(key) {
|
||||
const idx = this.profileForm.allowed_categories.indexOf(key);
|
||||
if (idx === -1) {
|
||||
this.profileForm.allowed_categories.push(key);
|
||||
} else {
|
||||
this.profileForm.allowed_categories.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
if (this.profileForm.allowed_categories.length === 0) {
|
||||
this.profileFormError = 'Select at least one category.';
|
||||
return;
|
||||
}
|
||||
this.profileFormError = null;
|
||||
this.savingProfile = true;
|
||||
try {
|
||||
const payload = {
|
||||
name: this.profileForm.name,
|
||||
description: this.profileForm.description || null,
|
||||
allowed_categories: this.profileForm.allowed_categories,
|
||||
};
|
||||
let resp;
|
||||
if (this.editingProfile) {
|
||||
resp = await fetch(`/api/imap-profiles/${this.editingProfile.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
resp = await fetch('/api/imap-profiles/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
this.profileFormError = data.detail || 'Failed to save profile.';
|
||||
return;
|
||||
}
|
||||
// Flatten response into the same shape used locally
|
||||
const normalized = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
owner_id: data.owner_id,
|
||||
allowed_categories: data.allowed_categories,
|
||||
is_builtin: data.is_builtin,
|
||||
};
|
||||
if (this.editingProfile) {
|
||||
const idx = this.profiles.findIndex(p => p.id === normalized.id);
|
||||
if (idx !== -1) this.profiles.splice(idx, 1, normalized);
|
||||
} else {
|
||||
this.profiles.push(normalized);
|
||||
}
|
||||
this.closeProfileModal();
|
||||
this.showAlert('success', 'Saved', this.editingProfile ? 'Profile updated.' : 'Profile created.');
|
||||
} catch (err) {
|
||||
this.profileFormError = `Network error: ${err.message || 'Unknown error'}. Please try again.`;
|
||||
} finally {
|
||||
this.savingProfile = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteProfile(profile) {
|
||||
this.profileToDelete = profile;
|
||||
this.deleteProfileModalOpen = true;
|
||||
},
|
||||
|
||||
async deleteProfile() {
|
||||
if (!this.profileToDelete) return;
|
||||
this.deletingProfile = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/imap-profiles/${this.profileToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
});
|
||||
if (resp.ok || resp.status === 204) {
|
||||
const deletedId = this.profileToDelete.id;
|
||||
this.profiles = this.profiles.filter(p => p.id !== deletedId);
|
||||
// Clear profile_id on accounts that referenced this profile
|
||||
this.accounts.forEach(a => { if (a.profile_id === deletedId) a.profile_id = null; });
|
||||
this.showAlert('success', 'Deleted', `Profile "${this.profileToDelete.name}" removed.`);
|
||||
this.deleteProfileModalOpen = false;
|
||||
this.profileToDelete = null;
|
||||
} else {
|
||||
const data = await resp.json();
|
||||
this.showAlert('error', 'Delete Failed', data.detail || 'Could not delete profile.');
|
||||
}
|
||||
} catch (err) {
|
||||
this.showAlert('error', 'Delete Failed', `Network error: ${err.message || 'Unknown error'}`);
|
||||
} finally {
|
||||
this.deletingProfile = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if multi_user_enabled and not is_logged_in %}DocuElevate – Intelligent Document Processing{% else %}Dashboard – DocuElevate{% endif %}{% endblock %}
|
||||
{% block title %}{% if multi_user_enabled and not is_logged_in %}{{ _("index.page_title_public") }}{% else %}{{ _("index.page_title_dashboard") }}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="{% if multi_user_enabled and not is_logged_in %}bg-gray-50 min-h-screen{% else %}container mx-auto px-4 py-8{% endif %}">
|
||||
@@ -13,30 +13,29 @@
|
||||
<div class="bg-gradient-to-br from-blue-700 via-indigo-700 to-purple-700 text-white py-20 px-4">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<span class="inline-block bg-white/20 text-white text-xs font-semibold uppercase tracking-widest px-3 py-1 rounded-full mb-4">
|
||||
Intelligent Document Processing
|
||||
{{ _("index.badge_intelligent") }}
|
||||
</span>
|
||||
<h1 class="text-4xl sm:text-5xl font-extrabold mb-4 leading-tight">
|
||||
From upload to insight — automatically.
|
||||
{{ _("index.hero_heading") }}
|
||||
</h1>
|
||||
<p class="text-indigo-100 text-lg max-w-2xl mx-auto mb-8">
|
||||
DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to
|
||||
Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.
|
||||
{{ _("index.hero_description") }}
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% if allow_signup %}
|
||||
<a href="/signup"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-user-plus mr-2" aria-hidden="true"></i> Get Started — it's free
|
||||
<i class="fas fa-user-plus mr-2" aria-hidden="true"></i> {{ _("index.hero_signup") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> Log In
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> {{ _("index.hero_login") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/pricing"
|
||||
class="bg-transparent border-2 border-white text-white hover:bg-white hover:text-indigo-700 font-bold py-3 px-8 rounded-xl transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> View Plans & Pricing
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> {{ _("index.hero_pricing") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +43,7 @@
|
||||
|
||||
<!-- Feature grid -->
|
||||
<div class="max-w-6xl mx-auto px-4 py-16">
|
||||
<h2 class="text-2xl font-bold text-center text-gray-800 mb-10">Everything you need for smart document workflows</h2>
|
||||
<h2 class="text-2xl font-bold text-center text-gray-800 mb-10">{{ _("index.feature_section_title") }}</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
@@ -52,8 +51,8 @@
|
||||
<i class="fas fa-file-alt text-blue-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">OCR & Text Extraction</h3>
|
||||
<p class="text-gray-500 text-sm">Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_ocr") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_ocr_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,8 +61,8 @@
|
||||
<i class="fas fa-brain text-indigo-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">AI Metadata Extraction</h3>
|
||||
<p class="text-gray-500 text-sm">OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_ai") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_ai_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,8 +71,8 @@
|
||||
<i class="fas fa-cloud-upload-alt text-green-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Multi-Cloud Storage</h3>
|
||||
<p class="text-gray-500 text-sm">Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_cloud") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_cloud_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,8 +81,8 @@
|
||||
<i class="fas fa-mail-bulk text-yellow-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Email & IMAP Ingestion</h3>
|
||||
<p class="text-gray-500 text-sm">Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_email") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_email_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,8 +91,8 @@
|
||||
<i class="fas fa-search text-purple-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Full-Text Search</h3>
|
||||
<p class="text-gray-500 text-sm">Instantly find any document by content, metadata, or tags across your entire archive.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_search") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_search_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,8 +101,8 @@
|
||||
<i class="fas fa-project-diagram text-pink-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Custom Pipelines</h3>
|
||||
<p class="text-gray-500 text-sm">Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">{{ _("index.feature_pipelines") }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.feature_pipelines_desc") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,23 +112,23 @@
|
||||
<!-- CTA banner -->
|
||||
<div class="bg-indigo-700 text-white py-14 px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="text-3xl font-extrabold mb-4">Ready to elevate your document workflow?</h2>
|
||||
<p class="text-indigo-200 mb-8">Join teams already automating their document processing with DocuElevate.</p>
|
||||
<h2 class="text-3xl font-extrabold mb-4">{{ _("index.cta_heading") }}</h2>
|
||||
<p class="text-indigo-200 mb-8">{{ _("index.cta_description") }}</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% if allow_signup %}
|
||||
<a href="/signup"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-rocket mr-2" aria-hidden="true"></i> Create a free account
|
||||
<i class="fas fa-rocket mr-2" aria-hidden="true"></i> {{ _("index.cta_signup") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> Log In
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> {{ _("index.hero_login") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/pricing"
|
||||
class="bg-transparent border-2 border-white text-white hover:bg-white hover:text-indigo-700 font-bold py-3 px-8 rounded-xl transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> See pricing
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> {{ _("index.cta_pricing") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,16 +144,16 @@
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">DocuElevate</h1>
|
||||
<p class="text-blue-100 text-sm">Intelligent document processing & management — built for teams</p>
|
||||
<p class="text-blue-100 text-sm">{{ _("index.dashboard_subtitle_teams") }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/upload"
|
||||
class="bg-white text-blue-700 hover:bg-blue-50 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center shadow-sm">
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i> Upload
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i> {{ _("index.button_upload") }}
|
||||
</a>
|
||||
<a href="/files"
|
||||
class="bg-transparent border border-white text-white hover:bg-white hover:text-blue-700 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center">
|
||||
<i class="fas fa-list mr-2" aria-hidden="true"></i> Browse Files
|
||||
<i class="fas fa-list mr-2" aria-hidden="true"></i> {{ _("index.button_browse_files") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +161,7 @@
|
||||
|
||||
<!-- Platform stats row (admin only) -->
|
||||
{% if is_admin %}
|
||||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">Platform overview</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">{{ _("index.platform_overview") }}</h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
|
||||
<div class="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
@@ -170,7 +169,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-extrabold text-blue-600">{{ stats.processed_files | default(0) }}</p>
|
||||
<p class="text-gray-500 text-xs">Total files</p>
|
||||
<p class="text-gray-500 text-xs">{{ _("index.stat_total_files") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
|
||||
@@ -179,7 +178,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-extrabold text-green-600">{{ stats.files_today | default(0) }}</p>
|
||||
<p class="text-gray-500 text-xs">Files today</p>
|
||||
<p class="text-gray-500 text-xs">{{ _("index.stat_files_today") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
|
||||
@@ -188,7 +187,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-extrabold text-indigo-600">{{ stats.files_month | default(0) }}</p>
|
||||
<p class="text-gray-500 text-xs">Files this month</p>
|
||||
<p class="text-gray-500 text-xs">{{ _("index.stat_files_month") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
|
||||
@@ -197,7 +196,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-extrabold text-purple-600">{{ stats.unique_users | default(0) }}</p>
|
||||
<p class="text-gray-500 text-xs">Active users</p>
|
||||
<p class="text-gray-500 text-xs">{{ _("index.stat_active_users") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,7 +204,7 @@
|
||||
|
||||
<!-- My usage / subscription widget -->
|
||||
{% if user_tier and user_usage %}
|
||||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">My usage</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">{{ _("index.usage_my_usage") }}</h2>
|
||||
<div class="bg-white rounded-xl shadow p-6 mb-8">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -220,16 +219,16 @@
|
||||
{% elif user_tier.id == 'professional' %}fa-star
|
||||
{% else %}fa-building{% endif %}
|
||||
mr-1" aria-hidden="true"></i>
|
||||
{{ user_tier.name }} Plan
|
||||
{{ user_tier.name }} {{ _("index.tier_plan") }}
|
||||
</span>
|
||||
{% if user_tier.id != 'business' %}
|
||||
<a href="/pricing" class="text-xs text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Upgrade <i class="fas fa-arrow-up-right-from-square ml-0.5 text-xs" aria-hidden="true"></i>
|
||||
{{ _("index.tier_upgrade") }} <i class="fas fa-arrow-up-right-from-square ml-0.5 text-xs" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/subscription" class="text-xs text-gray-500 hover:text-gray-700">
|
||||
View full details →
|
||||
{{ _("index.tier_view_details") }} →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -237,7 +236,7 @@
|
||||
<!-- Lifetime -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-gray-500">Lifetime files</span>
|
||||
<span class="text-gray-500">{{ _("index.usage_lifetime") }}</span>
|
||||
<span class="font-semibold text-gray-700">
|
||||
{{ user_usage.lifetime }}{% if user_tier.lifetime_file_limit > 0 %} / {{ user_tier.lifetime_file_limit }}{% endif %}
|
||||
</span>
|
||||
@@ -250,13 +249,13 @@
|
||||
style="width: {{ pct }}%"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-xs text-green-600">Unlimited</div>
|
||||
<div class="text-xs text-green-600">{{ _("index.usage_unlimited") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Today -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-gray-500">Files today</span>
|
||||
<span class="text-gray-500">{{ _("index.usage_today") }}</span>
|
||||
<span class="font-semibold text-gray-700">
|
||||
{{ user_usage.today }}{% if user_tier.daily_upload_limit > 0 %} / {{ user_tier.daily_upload_limit }}{% endif %}
|
||||
</span>
|
||||
@@ -269,13 +268,13 @@
|
||||
style="width: {{ pct }}%"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-xs text-green-600">Unlimited</div>
|
||||
<div class="text-xs text-green-600">{{ _("index.usage_unlimited") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- This month -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-gray-500">Files this month</span>
|
||||
<span class="text-gray-500">{{ _("index.usage_month") }}</span>
|
||||
<span class="font-semibold text-gray-700">
|
||||
{{ user_usage.month }}{% if user_tier.monthly_upload_limit > 0 %} / {{ user_tier.monthly_upload_limit }}{% endif %}
|
||||
</span>
|
||||
@@ -288,7 +287,7 @@
|
||||
style="width: {{ pct }}%"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-xs text-green-600">Unlimited</div>
|
||||
<div class="text-xs text-green-600">{{ _("index.usage_unlimited") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -300,7 +299,7 @@
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> Quick Actions
|
||||
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> {{ _("index.quick_actions") }}
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
@@ -309,8 +308,8 @@
|
||||
<i class="fas fa-upload text-blue-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">Upload Document</p>
|
||||
<p class="text-xs text-gray-400">Process a new file</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_upload") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_upload_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -320,8 +319,8 @@
|
||||
<i class="fas fa-list text-indigo-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">My Documents</p>
|
||||
<p class="text-xs text-gray-400">Browse your processed files</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_documents") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_documents_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -331,8 +330,8 @@
|
||||
<i class="fas fa-layer-group text-purple-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">My Subscription</p>
|
||||
<p class="text-xs text-gray-400">View plan & usage details</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_subscription") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_subscription_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -342,8 +341,8 @@
|
||||
<i class="fas fa-search text-green-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">Search</p>
|
||||
<p class="text-xs text-gray-400">Full-text search across documents</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_search") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_search_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -353,30 +352,30 @@
|
||||
<!-- Processing stats -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-chart-bar text-blue-500" aria-hidden="true"></i> Processing Stats
|
||||
<i class="fas fa-chart-bar text-blue-500" aria-hidden="true"></i> {{ _("index.processing_stats") }}
|
||||
</h2>
|
||||
<ul class="space-y-3 text-sm">
|
||||
<li class="flex items-center justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600 flex items-center gap-2">
|
||||
<i class="fas fa-file-alt text-blue-400 w-4" aria-hidden="true"></i> Total processed
|
||||
<i class="fas fa-file-alt text-blue-400 w-4" aria-hidden="true"></i> {{ _("index.stat_total_processed") }}
|
||||
</span>
|
||||
<span class="font-bold text-gray-900">{{ stats.processed_files | default(0) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600 flex items-center gap-2">
|
||||
<i class="fas fa-eye text-indigo-400 w-4" aria-hidden="true"></i> Files with OCR
|
||||
<i class="fas fa-eye text-indigo-400 w-4" aria-hidden="true"></i> {{ _("index.stat_files_ocr") }}
|
||||
</span>
|
||||
<span class="font-bold text-gray-900">{{ stats.files_with_ocr | default(0) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600 flex items-center gap-2">
|
||||
<i class="fas fa-calendar-day text-green-400 w-4" aria-hidden="true"></i> Today
|
||||
<i class="fas fa-calendar-day text-green-400 w-4" aria-hidden="true"></i> {{ _("index.stat_today") }}
|
||||
</span>
|
||||
<span class="font-bold text-gray-900">{{ stats.files_today | default(0) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-2">
|
||||
<span class="text-gray-600 flex items-center gap-2">
|
||||
<i class="fas fa-calendar-alt text-orange-400 w-4" aria-hidden="true"></i> This month
|
||||
<i class="fas fa-calendar-alt text-orange-400 w-4" aria-hidden="true"></i> {{ _("index.stat_this_month") }}
|
||||
</span>
|
||||
<span class="font-bold text-gray-900">{{ stats.files_month | default(0) }}</span>
|
||||
</li>
|
||||
@@ -388,39 +387,39 @@
|
||||
<div class="bg-gradient-to-br from-indigo-600 to-purple-700 rounded-xl shadow p-6 text-white flex flex-col justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<i class="fas fa-arrow-up-right-dots" aria-hidden="true"></i> Upgrade your plan
|
||||
<i class="fas fa-arrow-up-right-dots" aria-hidden="true"></i> {{ _("index.upgrade_plan") }}
|
||||
</h2>
|
||||
<p class="text-indigo-100 text-sm mb-4">
|
||||
Unlock more documents, more destinations and priority support.
|
||||
{{ _("index.upgrade_description") }}
|
||||
</p>
|
||||
<ul class="space-y-1.5 text-sm text-indigo-100">
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> Higher daily & monthly limits</li>
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> More storage destinations</li>
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> More OCR pages</li>
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> {{ _("index.upgrade_daily_limits") }}</li>
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> {{ _("index.upgrade_destinations") }}</li>
|
||||
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> {{ _("index.upgrade_ocr_pages") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/pricing"
|
||||
class="mt-6 inline-flex items-center justify-center bg-white text-indigo-700 font-semibold px-5 py-2.5 rounded-lg hover:bg-indigo-50 transition shadow">
|
||||
View plans & pricing <i class="fas fa-arrow-right ml-2 text-xs" aria-hidden="true"></i>
|
||||
{{ _("index.upgrade_view_pricing") }} <i class="fas fa-arrow-right ml-2 text-xs" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-plug text-green-500" aria-hidden="true"></i> Integrations
|
||||
<i class="fas fa-plug text-green-500" aria-hidden="true"></i> {{ _("index.integrations_title") }}
|
||||
</h2>
|
||||
<div class="space-y-3 text-sm text-gray-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Active integrations</span>
|
||||
<span>{{ _("index.integrations_active") }}</span>
|
||||
<span class="font-bold text-green-600">{{ stats.active_integrations | default(0) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Storage targets</span>
|
||||
<span>{{ _("index.integrations_storage") }}</span>
|
||||
<span class="font-bold text-indigo-600">{{ stats.storage_targets | default(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/status" class="mt-4 inline-flex items-center text-xs text-indigo-600 hover:text-indigo-800">
|
||||
View system status <i class="fas fa-arrow-right ml-1" aria-hidden="true"></i>
|
||||
{{ _("index.integrations_view_status") }} <i class="fas fa-arrow-right ml-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -435,15 +434,15 @@
|
||||
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-xl shadow-lg text-white p-8 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">DocuElevate Dashboard</h1>
|
||||
<p class="text-blue-100 text-sm">Intelligent document processing & management</p>
|
||||
<h1 class="text-3xl font-bold mb-1">{{ _("index.single_user_heading") }}</h1>
|
||||
<p class="text-blue-100 text-sm">{{ _("index.dashboard_subtitle") }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/upload" class="bg-white text-blue-700 hover:bg-blue-50 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center shadow-sm">
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i> Upload
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i> {{ _("index.button_upload") }}
|
||||
</a>
|
||||
<a href="/files" class="bg-transparent border border-white text-white hover:bg-white hover:text-blue-700 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center">
|
||||
<i class="fas fa-list mr-2" aria-hidden="true"></i> Browse Files
|
||||
<i class="fas fa-list mr-2" aria-hidden="true"></i> {{ _("index.button_browse_files") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -457,7 +456,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-blue-600">{{ stats.processed_files | default(0) }}</p>
|
||||
<p class="text-gray-500 text-sm">Documents Processed</p>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.stat_documents_processed") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6 flex items-center gap-4">
|
||||
@@ -466,7 +465,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-green-600">{{ stats.active_integrations | default(0) }}</p>
|
||||
<p class="text-gray-500 text-sm">Active Integrations</p>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.stat_active_integrations") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6 flex items-center gap-4">
|
||||
@@ -475,7 +474,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-indigo-600">{{ stats.storage_targets | default(0) }}</p>
|
||||
<p class="text-gray-500 text-sm">Storage Targets</p>
|
||||
<p class="text-gray-500 text-sm">{{ _("index.stat_storage_targets") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -486,7 +485,7 @@
|
||||
<!-- Quick Actions Widget -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> Quick Actions
|
||||
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> {{ _("index.quick_actions") }}
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
@@ -495,8 +494,8 @@
|
||||
<i class="fas fa-upload text-blue-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">Upload Document</p>
|
||||
<p class="text-xs text-gray-400">Process a new file</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_upload") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_upload_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -506,8 +505,8 @@
|
||||
<i class="fas fa-list text-indigo-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">View All Files</p>
|
||||
<p class="text-xs text-gray-400">Browse processed documents</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_view_files") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_view_files_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -517,8 +516,8 @@
|
||||
<i class="fas fa-heartbeat text-green-600 text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">System Status</p>
|
||||
<p class="text-xs text-gray-400">Check integration health</p>
|
||||
<p class="text-sm font-medium text-gray-800">{{ _("index.quick_system_status") }}</p>
|
||||
<p class="text-xs text-gray-400">{{ _("index.quick_system_status_desc") }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -528,28 +527,28 @@
|
||||
<!-- Capabilities Widget -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-cubes text-blue-500" aria-hidden="true"></i> Capabilities
|
||||
<i class="fas fa-cubes text-blue-500" aria-hidden="true"></i> {{ _("index.capabilities_title") }}
|
||||
</h2>
|
||||
<ul class="space-y-3 text-sm text-gray-600">
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
<span>OCR & metadata extraction with AI</span>
|
||||
<span>{{ _("index.capabilities_ocr") }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
<span>Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud</span>
|
||||
<span>{{ _("index.capabilities_cloud") }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
<span>Paperless-ngx integration for document management</span>
|
||||
<span>{{ _("index.capabilities_paperless") }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
<span>Email & URL-based document ingestion</span>
|
||||
<span>{{ _("index.capabilities_ingestion") }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
<span>Automated classification & routing workflows</span>
|
||||
<span>{{ _("index.capabilities_workflows") }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -558,25 +557,25 @@
|
||||
<div class="bg-white rounded-xl shadow p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-compass text-indigo-500" aria-hidden="true"></i> Getting Started
|
||||
<i class="fas fa-compass text-indigo-500" aria-hidden="true"></i> {{ _("index.getting_started") }}
|
||||
</h2>
|
||||
<ol class="space-y-3 text-sm text-gray-600 list-none">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="h-5 w-5 rounded-full bg-blue-600 text-white text-xs flex items-center justify-center flex-shrink-0 font-bold mt-0.5">1</span>
|
||||
Configure integrations via <a href="/status" class="text-blue-600 hover:underline">System Status</a>
|
||||
{{ _("index.getting_started_1_pre") }}<a href="/status" class="text-blue-600 hover:underline">{{ _("index.quick_system_status") }}</a>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="h-5 w-5 rounded-full bg-blue-600 text-white text-xs flex items-center justify-center flex-shrink-0 font-bold mt-0.5">2</span>
|
||||
<a href="/upload" class="text-blue-600 hover:underline">Upload</a> your first document
|
||||
<a href="/upload" class="text-blue-600 hover:underline">{{ _("index.button_upload") }}</a>{{ _("index.getting_started_2_post") }}
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="h-5 w-5 rounded-full bg-blue-600 text-white text-xs flex items-center justify-center flex-shrink-0 font-bold mt-0.5">3</span>
|
||||
Review results in <a href="/files" class="text-blue-600 hover:underline">Files</a>
|
||||
{{ _("index.getting_started_3_pre") }}<a href="/files" class="text-blue-600 hover:underline">{{ _("index.link_files") }}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<a href="/about" class="mt-6 inline-flex items-center text-sm text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Learn more about DocuElevate <i class="fas fa-arrow-right ml-1 text-xs" aria-hidden="true"></i>
|
||||
{{ _("index.getting_started_learn") }} <i class="fas fa-arrow-right ml-1 text-xs" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocuElevate - Login</title>
|
||||
<title>{{ _("app.name") }} - {{ _("auth.login_title") }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
||||
@@ -12,10 +12,10 @@
|
||||
<body class="bg-gray-100 h-screen flex items-center justify-center">
|
||||
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full" role="main">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
|
||||
<img src="/static/images/logo_writing.svg" alt="{{ _('auth.logo_alt') }}" class="h-16">
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">Welcome to DocuElevate</h1>
|
||||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">{{ _("dashboard.welcome") }}</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
||||
@@ -31,33 +31,33 @@
|
||||
|
||||
<!-- Local authentication form -->
|
||||
<div class="mb-8" id="local-auth">
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-700">Sign in with username</h2>
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-700">{{ _("auth.sign_in_with_username") }}</h2>
|
||||
<form method="POST" action="/auth" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token | default('', true) }}">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">Username or Email</label>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">{{ _("auth.username_or_email") }}</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
autocomplete="username"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">{{ _("auth.password_label") }}</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Sign in
|
||||
{{ _("auth.sign_in") }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-3 text-center space-x-3">
|
||||
<a href="/forgot-password" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
Forgot password?
|
||||
{{ _("auth.forgot_password") }}
|
||||
</a>
|
||||
<span class="text-gray-300" aria-hidden="true">|</span>
|
||||
<a href="/forgot-username" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
Forgot username?
|
||||
{{ _("auth.forgot_username") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,37 +69,64 @@
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
<span class="px-2 bg-white text-gray-500">{{ _("auth.or_continue_with") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-3">
|
||||
<a href="/oauth-login"
|
||||
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span class="sr-only">Sign in with SSO</span>
|
||||
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
style="min-height:44px; min-width:44px">
|
||||
<span class="sr-only">{{ _("auth.sign_in_sso") }}</span>
|
||||
<i class="fas fa-lock mr-2" aria-hidden="true"></i>
|
||||
{{ oauth_provider_name }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if social_providers %}
|
||||
<!-- Social login options -->
|
||||
{% if not show_oauth %}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">{{ _("auth.or_continue_with") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-3">
|
||||
{% for provider_key, provider in social_providers.items() %}
|
||||
<a href="/social-login/{{ provider_key }}"
|
||||
class="w-full inline-flex items-center justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
style="min-height:44px; min-width:44px"
|
||||
aria-label="{{ _('auth.sign_in_with') }} {{ provider.name }}">
|
||||
<i class="{{ provider.icon }} mr-2" aria-hidden="true"></i>
|
||||
{{ _("auth.sign_in_with") }} {{ provider.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="/" class="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
Return to Home
|
||||
{{ _("auth.return_home") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if allow_signup %}
|
||||
<div class="mt-4 text-center">
|
||||
<span class="text-sm text-gray-600">Don't have an account?</span>
|
||||
<span class="text-sm text-gray-600">{{ _("auth.no_account") }}</span>
|
||||
<a href="/signup" class="ml-1 text-sm font-medium text-indigo-600 hover:text-indigo-500">
|
||||
Create account
|
||||
{{ _("auth.create_account") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
<div class="fixed bottom-4 text-center w-full text-xs text-gray-500">
|
||||
DocuElevate {{ app_version|default('', true) }}
|
||||
{{ _("app.name") }} {{ app_version|default('', true) }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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,16 +310,16 @@
|
||||
@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>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -358,7 +358,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 +366,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 +379,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 +387,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 +400,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 +408,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 +446,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 +458,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 +471,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 +515,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>
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Profile Settings – DocuElevate{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="container mx-auto px-4 py-8 max-w-3xl"
|
||||
x-data="profileSettings()"
|
||||
x-init="init()"
|
||||
>
|
||||
|
||||
<!-- ── Header ─────────────────────────────────────────────────────────── -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<i class="fas fa-user-circle text-blue-500" aria-hidden="true"></i>
|
||||
Profile Settings
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage your display name, avatar, language, and theme preferences.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ── Status banner ──────────────────────────────────────────────────── -->
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
x-show="banner.visible"
|
||||
x-transition
|
||||
:class="banner.error
|
||||
? 'bg-red-50 dark:bg-red-900/30 border border-red-300 dark:border-red-700 text-red-800 dark:text-red-200'
|
||||
: 'bg-green-50 dark:bg-green-900/30 border border-green-300 dark:border-green-700 text-green-800 dark:text-green-200'"
|
||||
class="rounded-lg p-4 mb-6 flex items-start gap-3 text-sm"
|
||||
>
|
||||
<i :class="banner.error ? 'fas fa-exclamation-circle' : 'fas fa-check-circle'" aria-hidden="true"></i>
|
||||
<span x-text="banner.message"></span>
|
||||
<button
|
||||
type="button"
|
||||
@click="banner.visible = false"
|
||||
class="ml-auto"
|
||||
aria-label="Dismiss"
|
||||
style="min-height:44px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Avatar card ────────────────────────────────────────────────────── -->
|
||||
<section
|
||||
class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6"
|
||||
aria-labelledby="avatar-heading"
|
||||
>
|
||||
<h2 id="avatar-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
|
||||
<i class="fas fa-camera text-gray-400 mr-2" aria-hidden="true"></i>Profile Picture
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center gap-6">
|
||||
<!-- Current avatar -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<img
|
||||
:src="avatarUrl"
|
||||
alt="Your profile picture"
|
||||
class="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
<template x-if="hasCustomAvatar">
|
||||
<button
|
||||
type="button"
|
||||
@click="removeAvatar()"
|
||||
:disabled="saving"
|
||||
class="absolute -top-1 -right-1 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center shadow focus:outline-none focus:ring-2 focus:ring-red-400"
|
||||
aria-label="Remove custom avatar"
|
||||
title="Remove custom avatar"
|
||||
>
|
||||
<i class="fas fa-times text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Upload controls -->
|
||||
<div class="flex-1 space-y-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Upload a JPEG, PNG, GIF, or WebP image up to 2 MB.
|
||||
If no custom picture is set, your <a href="https://gravatar.com" class="underline" target="_blank" rel="noopener noreferrer" aria-label="Gravatar (opens in new tab)">Gravatar</a> is shown.
|
||||
</p>
|
||||
<label
|
||||
for="avatar-input"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md cursor-pointer focus-within:ring-2 focus-within:ring-blue-500 transition-colors"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-upload" aria-hidden="true"></i>
|
||||
<span>Choose image…</span>
|
||||
<input
|
||||
id="avatar-input"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
class="sr-only"
|
||||
@change="uploadAvatar($event)"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</label>
|
||||
<p x-show="uploadProgress" class="text-xs text-gray-500 dark:text-gray-400" x-text="uploadProgress" aria-live="polite"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── General info card ──────────────────────────────────────────────── -->
|
||||
<section
|
||||
class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6"
|
||||
aria-labelledby="general-heading"
|
||||
>
|
||||
<h2 id="general-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
|
||||
<i class="fas fa-id-card text-gray-400 mr-2" aria-hidden="true"></i>General Information
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Display name -->
|
||||
<div>
|
||||
<label for="display-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
id="display-name"
|
||||
type="text"
|
||||
x-model="form.display_name"
|
||||
maxlength="255"
|
||||
placeholder="Your name as shown in the UI"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
aria-describedby="display-name-hint"
|
||||
/>
|
||||
<p id="display-name-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Leave blank to use your account username or email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Contact email -->
|
||||
<div>
|
||||
<label for="contact-email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Contact / Notification E-mail
|
||||
</label>
|
||||
<input
|
||||
id="contact-email"
|
||||
type="email"
|
||||
x-model="form.contact_email"
|
||||
maxlength="255"
|
||||
placeholder="you@example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
aria-describedby="contact-email-hint"
|
||||
/>
|
||||
<p id="contact-email-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Used for system notifications. This does not change your login e-mail.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Preferences card ───────────────────────────────────────────────── -->
|
||||
<section
|
||||
class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6"
|
||||
aria-labelledby="prefs-heading"
|
||||
>
|
||||
<h2 id="prefs-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
|
||||
<i class="fas fa-sliders-h text-gray-400 mr-2" aria-hidden="true"></i>Preferences
|
||||
</h2>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Language -->
|
||||
<div>
|
||||
<label for="lang-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<i class="fas fa-globe text-gray-400 mr-1" aria-hidden="true"></i>UI Language
|
||||
</label>
|
||||
<select
|
||||
id="lang-select"
|
||||
x-model="form.preferred_language"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<option value="">Auto-detect (from browser)</option>
|
||||
{% for lang in supported_languages %}
|
||||
<option value="{{ lang.code }}">{{ lang.flag }} {{ lang.native }} ({{ lang.name }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Choose your preferred interface language. "Auto-detect" follows your browser settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme -->
|
||||
<fieldset>
|
||||
<legend class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<i class="fas fa-palette text-gray-400 mr-1" aria-hidden="true"></i>Colour Scheme
|
||||
</legend>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<label
|
||||
class="relative flex items-center gap-2 cursor-pointer rounded-lg border px-4 py-3 text-sm transition-colors
|
||||
focus-within:ring-2 focus-within:ring-blue-500"
|
||||
:class="form.preferred_theme === 'light'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-gray-300'"
|
||||
style="min-height:44px; min-width:44px;"
|
||||
>
|
||||
<input type="radio" name="theme" value="light" x-model="form.preferred_theme" class="sr-only" />
|
||||
<i class="fas fa-sun" aria-hidden="true"></i> Light
|
||||
</label>
|
||||
<label
|
||||
class="relative flex items-center gap-2 cursor-pointer rounded-lg border px-4 py-3 text-sm transition-colors
|
||||
focus-within:ring-2 focus-within:ring-blue-500"
|
||||
:class="form.preferred_theme === 'dark'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-gray-300'"
|
||||
style="min-height:44px; min-width:44px;"
|
||||
>
|
||||
<input type="radio" name="theme" value="dark" x-model="form.preferred_theme" class="sr-only" />
|
||||
<i class="fas fa-moon" aria-hidden="true"></i> Dark
|
||||
</label>
|
||||
<label
|
||||
class="relative flex items-center gap-2 cursor-pointer rounded-lg border px-4 py-3 text-sm transition-colors
|
||||
focus-within:ring-2 focus-within:ring-blue-500"
|
||||
:class="!form.preferred_theme || form.preferred_theme === 'system'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-gray-300'"
|
||||
style="min-height:44px; min-width:44px;"
|
||||
>
|
||||
<input type="radio" name="theme" value="system" x-model="form.preferred_theme" class="sr-only" />
|
||||
<i class="fas fa-desktop" aria-hidden="true"></i> System Default
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
"System Default" follows your device's dark-mode setting.
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Save button -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@click="saveProfile()"
|
||||
:disabled="saving"
|
||||
class="inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium
|
||||
rounded-md shadow focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 transition-colors"
|
||||
style="min-height:44px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-save" aria-hidden="true"></i>
|
||||
<span x-text="saving ? 'Saving…' : 'Save Changes'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Password change card (local accounts only) ─────────────────────── -->
|
||||
<template x-if="isLocalUser">
|
||||
<section
|
||||
class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mt-6"
|
||||
aria-labelledby="password-heading"
|
||||
>
|
||||
<h2 id="password-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
|
||||
<i class="fas fa-lock text-gray-400 mr-2" aria-hidden="true"></i>Change Password
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label for="current-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Current Password
|
||||
</label>
|
||||
<input
|
||||
id="current-password"
|
||||
type="password"
|
||||
x-model="pwForm.current_password"
|
||||
autocomplete="current-password"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
id="new-password"
|
||||
type="password"
|
||||
x-model="pwForm.new_password"
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
aria-describedby="new-pw-hint"
|
||||
/>
|
||||
<p id="new-pw-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">Minimum 8 characters.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
x-model="pwForm.new_password_confirm"
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
style="min-height:44px;"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@click="changePassword()"
|
||||
:disabled="pwSaving"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium
|
||||
rounded-md shadow focus:outline-none focus:ring-2 focus:ring-gray-500 disabled:opacity-50 transition-colors"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-key" aria-hidden="true"></i>
|
||||
<span x-text="pwSaving ? 'Updating…' : 'Update Password'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
</div><!-- /container -->
|
||||
|
||||
<script>
|
||||
function profileSettings() {
|
||||
return {
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
avatarUrl: '/static/images/avatar-placeholder.svg',
|
||||
hasCustomAvatar: false,
|
||||
isLocalUser: false,
|
||||
saving: false,
|
||||
pwSaving: false,
|
||||
uploadProgress: '',
|
||||
banner: { visible: false, error: false, message: '' },
|
||||
|
||||
form: {
|
||||
display_name: '',
|
||||
contact_email: '',
|
||||
preferred_language: '',
|
||||
preferred_theme: 'system',
|
||||
},
|
||||
|
||||
pwForm: {
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
new_password_confirm: '',
|
||||
},
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────────
|
||||
async init() {
|
||||
try {
|
||||
const res = await fetch('/api/profile');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
this.avatarUrl = data.avatar_url || this.avatarUrl;
|
||||
this.hasCustomAvatar = !!(data.avatar_url && data.avatar_url.startsWith('data:'));
|
||||
this.isLocalUser = data.is_local_user || false;
|
||||
this.form.display_name = data.display_name || '';
|
||||
this.form.contact_email = data.contact_email || '';
|
||||
this.form.preferred_language = data.preferred_language || '';
|
||||
this.form.preferred_theme = data.preferred_theme || 'system';
|
||||
} catch (_e) {
|
||||
// Silently ignore — user might not be logged in (rare for this page)
|
||||
}
|
||||
},
|
||||
|
||||
// ── CSRF helper ────────────────────────────────────────────────────────
|
||||
_getCSRFToken() {
|
||||
return document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('csrf_token='))
|
||||
?.split('=')[1];
|
||||
},
|
||||
|
||||
// ── Save general settings ──────────────────────────────────────────────
|
||||
async saveProfile() {
|
||||
this.saving = true;
|
||||
this._hideBanner();
|
||||
try {
|
||||
const csrfToken = this._getCSRFToken();
|
||||
|
||||
const res = await fetch('/api/profile', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
this._showBanner(data.detail || 'Failed to save settings.', true);
|
||||
return;
|
||||
}
|
||||
this._showBanner('Profile settings saved successfully.', false);
|
||||
// Update display name across the nav (refresh the avatar/name)
|
||||
if (data.avatar_url) {
|
||||
this.avatarUrl = data.avatar_url;
|
||||
this.hasCustomAvatar = data.avatar_url.startsWith('data:');
|
||||
}
|
||||
} catch (_e) {
|
||||
this._showBanner('Network error — please try again.', true);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Avatar upload ──────────────────────────────────────────────────────
|
||||
async uploadAvatar(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
this._showBanner('Image is too large. Maximum size is 2 MB.', true);
|
||||
return;
|
||||
}
|
||||
this.uploadProgress = 'Uploading…';
|
||||
this._hideBanner();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const csrfToken = this._getCSRFToken();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/profile/avatar', {
|
||||
method: 'POST',
|
||||
headers: csrfToken ? { 'X-CSRF-Token': csrfToken } : {},
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
this._showBanner(data.detail || 'Upload failed.', true);
|
||||
return;
|
||||
}
|
||||
this.avatarUrl = data.avatar_url;
|
||||
this.hasCustomAvatar = true;
|
||||
this._showBanner('Profile picture updated.', false);
|
||||
} catch (_e) {
|
||||
this._showBanner('Network error — please try again.', true);
|
||||
} finally {
|
||||
this.uploadProgress = '';
|
||||
// Reset file input so the same file can be re-selected
|
||||
event.target.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
// ── Remove avatar ──────────────────────────────────────────────────────
|
||||
async removeAvatar() {
|
||||
this._hideBanner();
|
||||
const csrfToken = this._getCSRFToken();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/profile/avatar', {
|
||||
method: 'DELETE',
|
||||
headers: csrfToken ? { 'X-CSRF-Token': csrfToken } : {},
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
this._showBanner(data.detail || 'Failed to remove avatar.', true);
|
||||
return;
|
||||
}
|
||||
this.avatarUrl = data.avatar_url;
|
||||
this.hasCustomAvatar = false;
|
||||
this._showBanner('Custom avatar removed. Gravatar is now shown.', false);
|
||||
} catch (_e) {
|
||||
this._showBanner('Network error — please try again.', true);
|
||||
}
|
||||
},
|
||||
|
||||
// ── Change password ────────────────────────────────────────────────────
|
||||
async changePassword() {
|
||||
this.pwSaving = true;
|
||||
this._hideBanner();
|
||||
const csrfToken = this._getCSRFToken();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/profile/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify(this.pwForm),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
this._showBanner(data.detail || 'Failed to change password.', true);
|
||||
return;
|
||||
}
|
||||
this._showBanner('Password changed successfully.', false);
|
||||
this.pwForm = { current_password: '', new_password: '', new_password_confirm: '' };
|
||||
} catch (_e) {
|
||||
this._showBanner('Network error — please try again.', true);
|
||||
} finally {
|
||||
this.pwSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Banner helpers ─────────────────────────────────────────────────────
|
||||
_showBanner(message, error) {
|
||||
this.banner = { visible: true, error, message };
|
||||
if (!error) {
|
||||
setTimeout(() => { this.banner.visible = false; }, 4000);
|
||||
}
|
||||
},
|
||||
_hideBanner() {
|
||||
this.banner.visible = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Queue Monitor — DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("queue.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<script src="/static/js/common.js"></script>
|
||||
@@ -11,20 +11,20 @@
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2">
|
||||
<i class="fas fa-stream mr-2 text-blue-500" aria-hidden="true"></i>Queue Monitor
|
||||
<i class="fas fa-stream mr-2 text-blue-500" aria-hidden="true"></i>{{ _("queue.heading") }}
|
||||
</h1>
|
||||
<p class="text-gray-600">Real-time view of the document processing pipeline and Celery task queues.</p>
|
||||
<p class="text-gray-600">{{ _("queue.subtitle") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div id="queueLoading" class="text-center py-12">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-blue-400" aria-hidden="true"></i>
|
||||
<p class="mt-2 text-gray-500">Loading queue statistics…</p>
|
||||
<p class="mt-2 text-gray-500">{{ _("queue.loading_stats") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div id="queueError" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
||||
<p><strong>Error:</strong> <span id="queueErrorMsg"></span></p>
|
||||
<p><strong>{{ _("common.error") }}:</strong> <span id="queueErrorMsg"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard content (hidden until loaded) -->
|
||||
@@ -39,7 +39,7 @@
|
||||
<i class="fas fa-inbox text-yellow-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Queued Tasks</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.queued_tasks") }}</p>
|
||||
<p id="totalQueued" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
<i class="fas fa-cogs text-blue-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Active Tasks</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.active_tasks") }}</p>
|
||||
<p id="activeTasks" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<i class="fas fa-file-alt text-indigo-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Files Processing</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.files_processing") }}</p>
|
||||
<p id="filesProcessing" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<i class="fas fa-server text-green-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Workers Online</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.workers_online") }}</p>
|
||||
<p id="workersOnline" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,19 +89,19 @@
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-list-ol mr-2 text-gray-500" aria-hidden="true"></i>Redis Queues
|
||||
<i class="fas fa-list-ol mr-2 text-gray-500" aria-hidden="true"></i>{{ _("queue.redis_queues") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">Queue</th>
|
||||
<th class="pb-2 text-right">Pending</th>
|
||||
<th class="pb-2">{{ _("queue.col_queue") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("common.pending") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueRows">
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">No data</td></tr>
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">{{ _("queue.no_data") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -111,19 +111,19 @@
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-database mr-2 text-gray-500" aria-hidden="true"></i>Processing Pipeline
|
||||
<i class="fas fa-database mr-2 text-gray-500" aria-hidden="true"></i>{{ _("queue.processing_pipeline") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">State</th>
|
||||
<th class="pb-2 text-right">Files</th>
|
||||
<th class="pb-2">{{ _("queue.col_state") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("queue.col_files") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbSummaryRows">
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">No data</td></tr>
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">{{ _("queue.no_data") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-play-circle mr-2 text-blue-500" aria-hidden="true"></i>Active Tasks
|
||||
<i class="fas fa-play-circle mr-2 text-blue-500" aria-hidden="true"></i>{{ _("queue.active_tasks") }}
|
||||
<span id="activeCount" class="ml-1 text-sm font-normal text-gray-500">(0)</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -142,13 +142,13 @@
|
||||
<table class="w-full text-sm" id="activeTable">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">Task</th>
|
||||
<th class="pb-2">Arguments</th>
|
||||
<th class="pb-2">Task ID</th>
|
||||
<th class="pb-2">{{ _("queue.col_task") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_arguments") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_task_id") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="activeRows">
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">No active tasks</td></tr>
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">{{ _("queue.no_active_tasks") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -158,20 +158,20 @@
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-file-medical-alt mr-2 text-indigo-500" aria-hidden="true"></i>Recently Processing Files
|
||||
<i class="fas fa-file-medical-alt mr-2 text-indigo-500" aria-hidden="true"></i>{{ _("queue.recently_processing") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">File</th>
|
||||
<th class="pb-2">Current Step</th>
|
||||
<th class="pb-2 text-right">Actions</th>
|
||||
<th class="pb-2">{{ _("queue.col_file") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_current_step") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentRows">
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">No files currently processing</td></tr>
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">{{ _("queue.no_files_processing") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -180,8 +180,8 @@
|
||||
<!-- Auto-refresh info -->
|
||||
<div class="text-center text-gray-400 text-xs mb-4">
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>
|
||||
Auto-refreshes every <span id="refreshInterval">10</span> seconds —
|
||||
Last updated: <span id="lastUpdated">—</span>
|
||||
{{ _("queue.auto_refresh_1") }} <span id="refreshInterval">10</span> {{ _("queue.auto_refresh_2") }} —
|
||||
{{ _("queue.last_updated") }} <span id="lastUpdated">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Search Documents - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("search.page_title") }} - DocuElevate{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<script src="/static/js/common.js"></script>
|
||||
@@ -121,74 +121,74 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="search-container">
|
||||
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500" aria-hidden="true"></i> Document Search</h1>
|
||||
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500" aria-hidden="true"></i> {{ _("search.heading") }}</h1>
|
||||
|
||||
<!-- Search box -->
|
||||
<div class="search-box" role="search">
|
||||
<label for="search-input" class="sr-only">Search documents</label>
|
||||
<label for="search-input" class="sr-only">{{ _("search.input_aria_label") }}</label>
|
||||
<input
|
||||
type="search"
|
||||
id="search-input"
|
||||
placeholder="Search documents by content, sender, tags, type..."
|
||||
placeholder="{{ _('search.input_placeholder') }}"
|
||||
value="{{ query }}"
|
||||
autofocus
|
||||
>
|
||||
<button type="button" id="search-btn" aria-label="Search documents">
|
||||
<i class="fas fa-search" aria-hidden="true"></i> Search
|
||||
<button type="button" id="search-btn" aria-label="{{ _('search.input_aria_label') }}">
|
||||
<i class="fas fa-search" aria-hidden="true"></i> {{ _("search.button") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content-finding filters -->
|
||||
<div class="search-filters" id="search-filters">
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-document-type">Document Type</label>
|
||||
<input type="text" id="filter-document-type" placeholder="e.g. Invoice" aria-label="Filter by document type">
|
||||
<label for="filter-document-type">{{ _("search.filter_document_type") }}</label>
|
||||
<input type="text" id="filter-document-type" placeholder="{{ _('search.filter_document_type_placeholder') }}" aria-label="{{ _('search.filter_aria_document_type') }}">
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-tags">Tags</label>
|
||||
<input type="text" id="filter-tags" placeholder="e.g. amazon" aria-label="Filter by tag">
|
||||
<label for="filter-tags">{{ _("search.filter_tags") }}</label>
|
||||
<input type="text" id="filter-tags" placeholder="{{ _('search.filter_tags_placeholder') }}" aria-label="{{ _('search.filter_aria_tag') }}">
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-sender">Sender</label>
|
||||
<input type="text" id="filter-sender" placeholder="e.g. ACME Corp" aria-label="Filter by sender">
|
||||
<label for="filter-sender">{{ _("search.filter_sender") }}</label>
|
||||
<input type="text" id="filter-sender" placeholder="{{ _('search.filter_sender_placeholder') }}" aria-label="{{ _('search.filter_aria_sender') }}">
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-language">Language</label>
|
||||
<input type="text" id="filter-language" placeholder="e.g. de" aria-label="Filter by language code">
|
||||
<label for="filter-language">{{ _("search.filter_language") }}</label>
|
||||
<input type="text" id="filter-language" placeholder="{{ _('search.filter_language_placeholder') }}" aria-label="{{ _('search.filter_aria_language') }}">
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-text-quality">Text Quality</label>
|
||||
<select id="filter-text-quality" aria-label="Filter by OCR text quality">
|
||||
<option value="">All</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="no_text">No text</option>
|
||||
<label for="filter-text-quality">{{ _("search.filter_text_quality") }}</label>
|
||||
<select id="filter-text-quality" aria-label="{{ _('search.filter_aria_text_quality') }}">
|
||||
<option value="">{{ _("search.filter_text_quality_all") }}</option>
|
||||
<option value="high">{{ _("search.filter_text_quality_high") }}</option>
|
||||
<option value="medium">{{ _("search.filter_text_quality_medium") }}</option>
|
||||
<option value="low">{{ _("search.filter_text_quality_low") }}</option>
|
||||
<option value="no_text">{{ _("search.filter_text_quality_no_text") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-date-from">Date From</label>
|
||||
<input type="date" id="filter-date-from" aria-label="Filter from date">
|
||||
<label for="filter-date-from">{{ _("search.filter_date_from") }}</label>
|
||||
<input type="date" id="filter-date-from" aria-label="{{ _('search.filter_aria_date_from') }}">
|
||||
</div>
|
||||
<div class="search-filter-item">
|
||||
<label for="filter-date-to">Date To</label>
|
||||
<input type="date" id="filter-date-to" aria-label="Filter to date">
|
||||
<label for="filter-date-to">{{ _("search.filter_date_to") }}</label>
|
||||
<input type="date" id="filter-date-to" aria-label="{{ _('search.filter_aria_date_to') }}">
|
||||
</div>
|
||||
<div class="search-filter-item" style="align-self: flex-end;">
|
||||
<button type="button" id="clear-filters-btn" style="padding: 0.4rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.8rem; cursor: pointer; background: white; color: #374151;" aria-label="Clear all filters">
|
||||
Clear Filters
|
||||
<button type="button" id="clear-filters-btn" style="padding: 0.4rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.8rem; cursor: pointer; background: white; color: #374151;" aria-label="{{ _('search.filter_aria_clear') }}">
|
||||
{{ _("search.filter_clear_button") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Saved Searches -->
|
||||
<div class="search-saved-bar" id="saved-searches-bar">
|
||||
<span class="saved-label"><i class="fas fa-bookmark" aria-hidden="true"></i> Saved Searches:</span>
|
||||
<span class="saved-label"><i class="fas fa-bookmark" aria-hidden="true"></i> {{ _("search.saved_label") }}:</span>
|
||||
<span id="saved-searches-list" aria-live="polite" style="display: flex; gap: 0.25rem; flex-wrap: wrap;">
|
||||
<span style="color: #9ca3af; font-size: 0.85rem;">Loading...</span>
|
||||
<span style="color: #9ca3af; font-size: 0.85rem;">{{ _("search.saved_loading") }}</span>
|
||||
</span>
|
||||
<button type="button" id="save-search-btn" class="btn-save-search" aria-label="Save current search and filters as a saved search">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> Save Current
|
||||
<button type="button" id="save-search-btn" class="btn-save-search" aria-label="{{ _('search.saved_aria_save') }}">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> {{ _("search.saved_button") }}
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Shared Links – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("shared.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="sharedLinks()" x-init="init(); loadLinks()" class="container mx-auto px-4 py-8 max-w-5xl">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<i class="fas fa-share-alt text-blue-500" aria-hidden="true"></i>
|
||||
Shared Links
|
||||
{{ _("shared.heading") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400 text-sm leading-relaxed max-w-2xl">
|
||||
Share documents with anyone via a time-limited or view-limited link.
|
||||
Recipients do not need a DocuElevate account. Links can be password-protected
|
||||
and revoked at any time.
|
||||
{{ _("shared.subtitle") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Create link section -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="create-link-heading">
|
||||
<h2 id="create-link-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Create New Shared Link</h2>
|
||||
<h2 id="create-link-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("shared.create_heading") }}</h2>
|
||||
|
||||
<form @submit.prevent="createLink()" class="space-y-4">
|
||||
<!-- File ID -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-start sm:items-end">
|
||||
<div class="flex-1">
|
||||
<label for="file-id-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
File ID <span class="text-red-500" aria-hidden="true">*</span>
|
||||
{{ _("shared.file_id_label") }} <span class="text-red-500" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-id-input"
|
||||
type="number"
|
||||
x-model.number="newLink.file_id"
|
||||
placeholder="e.g. 42"
|
||||
placeholder="{{ _('shared.file_id_placeholder') }}"
|
||||
required
|
||||
min="1"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -39,20 +37,20 @@
|
||||
aria-required="true"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Find the file ID on the <a href="/files" class="underline hover:text-blue-600">Files</a> page or in the document detail URL.
|
||||
{{ _("shared.file_id_help_pre") }} <a href="/files" class="underline hover:text-blue-600">{{ _("shared.files_link") }}</a> {{ _("shared.file_id_help_post") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Label (optional) -->
|
||||
<div class="flex-1">
|
||||
<label for="link-label-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Label <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.label_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="link-label-input"
|
||||
type="text"
|
||||
x-model="newLink.label"
|
||||
placeholder="e.g. Shared with Bob"
|
||||
placeholder="{{ _('shared.label_placeholder') }}"
|
||||
maxlength="255"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
@@ -65,7 +63,7 @@
|
||||
<!-- Expiry -->
|
||||
<div class="flex-1">
|
||||
<label for="expires-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Expiry
|
||||
{{ _("shared.expiry_label") }}
|
||||
</label>
|
||||
<select
|
||||
id="expires-select"
|
||||
@@ -73,28 +71,28 @@
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
>
|
||||
<option :value="null">Never</option>
|
||||
<option value="1">1 hour</option>
|
||||
<option value="6">6 hours</option>
|
||||
<option value="12">12 hours</option>
|
||||
<option value="24">24 hours (1 day)</option>
|
||||
<option value="72">3 days</option>
|
||||
<option value="168">7 days</option>
|
||||
<option value="336">14 days</option>
|
||||
<option value="720">30 days</option>
|
||||
<option :value="null">{{ _("shared.expiry_never") }}</option>
|
||||
<option value="1">{{ _("shared.expiry_1h") }}</option>
|
||||
<option value="6">{{ _("shared.expiry_6h") }}</option>
|
||||
<option value="12">{{ _("shared.expiry_12h") }}</option>
|
||||
<option value="24">{{ _("shared.expiry_24h") }}</option>
|
||||
<option value="72">{{ _("shared.expiry_3d") }}</option>
|
||||
<option value="168">{{ _("shared.expiry_7d") }}</option>
|
||||
<option value="336">{{ _("shared.expiry_14d") }}</option>
|
||||
<option value="720">{{ _("shared.expiry_30d") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Max views -->
|
||||
<div class="flex-1">
|
||||
<label for="max-views-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Max downloads <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.max_downloads_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="max-views-input"
|
||||
type="number"
|
||||
x-model.number="newLink.max_views"
|
||||
placeholder="Unlimited"
|
||||
placeholder="{{ _('shared.unlimited_placeholder') }}"
|
||||
min="1"
|
||||
max="10000"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -105,13 +103,13 @@
|
||||
<!-- Password -->
|
||||
<div class="flex-1">
|
||||
<label for="link-password-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.password_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="link-password-input"
|
||||
type="password"
|
||||
x-model="newLink.password"
|
||||
placeholder="Leave blank for no password"
|
||||
placeholder="{{ _('shared.password_placeholder') }}"
|
||||
maxlength="128"
|
||||
autocomplete="new-password"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -135,7 +133,7 @@
|
||||
style="min-height:40px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-share-alt mr-2" aria-hidden="true"></i>
|
||||
<span x-text="creating ? 'Creating…' : 'Create Link'"></span>
|
||||
<span x-text="creating ? '{{ _('shared.creating') }}' : '{{ _('shared.create_btn') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -146,9 +144,9 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-0.5 text-lg" aria-hidden="true"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">Shared link created!</p>
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">{{ _("shared.link_created") }}</p>
|
||||
<p class="text-green-700 dark:text-green-300 text-xs mt-1">
|
||||
Copy and send this link to the recipient.
|
||||
{{ _("shared.link_created_help") }}
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code
|
||||
@@ -164,10 +162,10 @@
|
||||
hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
transition-colors"
|
||||
style="min-height:40px; min-width:44px;"
|
||||
:aria-label="copied ? 'Copied!' : 'Copy link to clipboard'"
|
||||
:aria-label="copied ? '{{ _('common.copied') }}' : '{{ _('shared.copy_link_aria') }}'"
|
||||
>
|
||||
<i :class="copied ? 'fas fa-check text-green-600' : 'fas fa-copy'" aria-hidden="true"></i>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? 'Copied!' : 'Copy'"></span>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? '{{ _('common.copied') }}' : '{{ _('common.copy') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,28 +178,28 @@
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6" aria-labelledby="links-heading">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 id="links-heading" class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Your Shared Links
|
||||
{{ _("shared.your_links") }}
|
||||
<span
|
||||
x-show="links.length > 0"
|
||||
class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
x-text="links.length"
|
||||
aria-label="number of links"
|
||||
aria-label="{{ _('shared.links_count_aria') }}"
|
||||
></span>
|
||||
</h2>
|
||||
<button
|
||||
@click="loadLinks()"
|
||||
type="button"
|
||||
class="text-sm text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
|
||||
aria-label="Refresh shared links list"
|
||||
aria-label="{{ _('shared.refresh_aria') }}"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>Refresh
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>{{ _("common.refresh") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<template x-if="loading">
|
||||
<p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>Loading…
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>{{ _("common.loading") }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -209,22 +207,22 @@
|
||||
<template x-if="!loading && links.length === 0">
|
||||
<div class="text-center py-10 text-gray-400 dark:text-gray-500">
|
||||
<i class="fas fa-share-alt text-3xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-sm">No shared links yet. Create one above to get started.</p>
|
||||
<p class="text-sm">{{ _("shared.no_links") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Links list -->
|
||||
<template x-if="!loading && links.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm" aria-label="Shared links">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm" aria-label="{{ _('shared.table_aria') }}">
|
||||
<thead>
|
||||
<tr class="text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th scope="col" class="pb-3 pr-4">File / Label</th>
|
||||
<th scope="col" class="pb-3 pr-4">Link</th>
|
||||
<th scope="col" class="pb-3 pr-4">Expiry</th>
|
||||
<th scope="col" class="pb-3 pr-4">Views</th>
|
||||
<th scope="col" class="pb-3 pr-4">Status</th>
|
||||
<th scope="col" class="pb-3">Actions</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_file_label") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_link") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_expiry") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_views") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="pb-3">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@@ -235,7 +233,7 @@
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200 truncate" x-text="link.original_filename || ('File #' + link.file_id)"></p>
|
||||
<p x-show="link.label" class="text-xs text-gray-500 dark:text-gray-400 truncate" x-text="link.label"></p>
|
||||
<p x-show="link.has_password" class="text-xs text-yellow-600 dark:text-yellow-400 mt-0.5">
|
||||
<i class="fas fa-lock text-xs" aria-hidden="true"></i> Password protected
|
||||
<i class="fas fa-lock text-xs" aria-hidden="true"></i> {{ _("shared.password_protected") }}
|
||||
</p>
|
||||
</td>
|
||||
<!-- Link -->
|
||||
@@ -246,8 +244,8 @@
|
||||
type="button"
|
||||
@click="copyUrl(link.share_url)"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
aria-label="Copy link"
|
||||
title="Copy link"
|
||||
aria-label="{{ _('shared.copy_link_aria') }}"
|
||||
title="{{ _('shared.copy_link_title') }}"
|
||||
style="min-height:32px;min-width:32px;"
|
||||
>
|
||||
<i class="fas fa-copy text-xs" aria-hidden="true"></i>
|
||||
@@ -257,8 +255,8 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="p-1 text-gray-400 hover:text-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
aria-label="Open shared link"
|
||||
title="Open in new tab"
|
||||
aria-label="{{ _('shared.open_link_aria') }}"
|
||||
title="{{ _('shared.open_in_new_tab') }}"
|
||||
style="min-height:32px;min-width:32px;"
|
||||
>
|
||||
<i class="fas fa-external-link-alt text-xs" aria-hidden="true"></i>
|
||||
@@ -278,22 +276,22 @@
|
||||
<td class="py-3 pr-4 whitespace-nowrap">
|
||||
<template x-if="!link.is_active">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
|
||||
Revoked
|
||||
{{ _("shared.status_revoked") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && isExpired(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300">
|
||||
Expired
|
||||
{{ _("shared.status_expired") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && isViewLimitReached(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">
|
||||
Limit reached
|
||||
{{ _("shared.status_limit_reached") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && !isExpired(link) && !isViewLimitReached(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
Active
|
||||
{{ _("shared.status_active") }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
@@ -307,9 +305,9 @@
|
||||
border border-red-300 dark:border-red-700 rounded hover:bg-red-50 dark:hover:bg-red-900/20
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors"
|
||||
style="min-height:32px;min-width:44px;"
|
||||
:aria-label="'Revoke shared link for ' + (link.original_filename || 'file')"
|
||||
:aria-label="'{{ _('shared.revoke_aria_prefix') }} ' + (link.original_filename || '{{ _('shared.revoke_aria_file') }}')"
|
||||
>
|
||||
<i class="fas fa-ban mr-1" aria-hidden="true"></i> Revoke
|
||||
<i class="fas fa-ban mr-1" aria-hidden="true"></i> {{ _("shared.revoke_btn") }}
|
||||
</button>
|
||||
<span x-show="!link.is_active" class="text-xs text-gray-400 dark:text-gray-500">—</span>
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Document Similarity - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("similarity.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
@@ -97,45 +97,43 @@
|
||||
<main id="main-content" class="sim-container px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-2">
|
||||
<i class="fas fa-project-diagram text-blue-500" aria-hidden="true"></i>
|
||||
Document Similarity
|
||||
{{ _("similarity.heading") }}
|
||||
</h1>
|
||||
<p class="text-gray-500 mb-6 text-sm">
|
||||
Pairs of documents with high semantic similarity, ranked by score.
|
||||
Embeddings are computed during document ingestion; a background task
|
||||
also backfills any files that were processed before this feature was enabled.
|
||||
{{ _("similarity.subtitle") }}
|
||||
</p>
|
||||
|
||||
<!-- Embedding coverage stats -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ total_files }}</div>
|
||||
<div class="stat-label">Total Files</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_total_files") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #2563eb;">{{ files_with_embedding }}</div>
|
||||
<div class="stat-label">With Embedding</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_with_embedding") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: {% if files_missing_embedding > 0 %}#d97706{% else %}#059669{% endif %};">
|
||||
{{ files_missing_embedding }}
|
||||
</div>
|
||||
<div class="stat-label">Missing Embedding</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_missing_embedding") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-sm" style="word-break: break-all;">{{ embedding_model }}</div>
|
||||
<div class="stat-label">Embedding Model</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_embedding_model") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if files_missing_embedding > 0 %}
|
||||
<div style="background: #fffff0; border: 1px solid #fefcbf; border-radius: 0.5rem; padding: 0.75rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: #975a16;">
|
||||
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<strong>{{ files_missing_embedding }}</strong> file(s) have OCR text but no embedding yet.
|
||||
The background task will compute them automatically every 5 minutes, or you can
|
||||
<strong>{{ files_missing_embedding }}</strong> {{ _("similarity.files_missing_text") }}
|
||||
{{ _("similarity.backfill_auto") }}
|
||||
<button onclick="triggerBackfill()" id="backfill-btn"
|
||||
style="background: #d97706; color: white; border: none; padding: 0.2rem 0.6rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem; min-height: 44px; min-width: 44px;"
|
||||
aria-label="Trigger embedding computation for all files missing embeddings">
|
||||
trigger it now
|
||||
aria-label="{{ _('similarity.trigger_aria') }}">
|
||||
{{ _("similarity.trigger_now") }}
|
||||
</button>.
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -144,12 +142,12 @@
|
||||
<div class="controls">
|
||||
<form id="pairsForm" onsubmit="loadPairs(event)" class="controls-form">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="pairThreshold">Min. similarity</label>
|
||||
<label for="pairThreshold">{{ _("similarity.min_similarity_label") }}</label>
|
||||
<input type="number" id="pairThreshold" min="0" max="1" step="0.05"
|
||||
value="{{ default_threshold }}" style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="pairLimit">Per page</label>
|
||||
<label for="pairLimit">{{ _("similarity.per_page_label") }}</label>
|
||||
<select id="pairLimit" style="min-height:44px;">
|
||||
<option value="25">25</option>
|
||||
<option value="50" selected>50</option>
|
||||
@@ -159,7 +157,7 @@
|
||||
<button type="submit"
|
||||
style="min-height:44px; background-color: #2563eb; color: white; font-weight: 700;
|
||||
padding: 0 1.25rem; border: none; border-radius: 0.5rem; cursor: pointer;">
|
||||
<i class="fas fa-search" aria-hidden="true"></i> Find Pairs
|
||||
<i class="fas fa-search" aria-hidden="true"></i> {{ _("similarity.find_pairs_btn") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -167,23 +165,23 @@
|
||||
<!-- Results area -->
|
||||
<div id="pairs-loading" style="text-align: center; padding: 2rem; color: #718096;">
|
||||
<i class="fas fa-spinner fa-spin" aria-hidden="true" style="font-size: 1.5rem; margin-bottom: 0.5rem;"></i>
|
||||
<p>Scanning for similar document pairs…</p>
|
||||
<p>{{ _("similarity.scanning") }}</p>
|
||||
</div>
|
||||
<div id="pairs-content" style="display: none;" aria-live="polite"></div>
|
||||
<div id="pairs-empty" style="display: none;" class="empty-state">
|
||||
<i class="fas fa-check-circle text-green-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700">No similar pairs found</p>
|
||||
<p class="font-semibold text-lg text-gray-700">{{ _("similarity.no_pairs_heading") }}</p>
|
||||
<p class="text-sm mt-1" id="pairs-empty-detail">
|
||||
No document pairs exceed the similarity threshold.
|
||||
{{ _("similarity.no_pairs_detail") }}
|
||||
</p>
|
||||
</div>
|
||||
<div id="pairs-error" style="display: none;" class="empty-state">
|
||||
<i class="fas fa-exclamation-triangle text-red-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700" id="pairs-error-msg">Failed to load pairs.</p>
|
||||
<p class="font-semibold text-lg text-gray-700" id="pairs-error-msg">{{ _("similarity.load_error") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav id="pairs-pagination" class="pagination" style="display: none;" aria-label="Similarity pairs pagination"></nav>
|
||||
<nav id="pairs-pagination" class="pagination" style="display: none;" aria-label="{{ _('similarity.pagination_aria') }}"></nav>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Upload Document{% endblock %}
|
||||
{% block title %}{{ _("upload.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col items-center justify-center p-4 sm:p-8">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">Upload Files</h1>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">{{ _("upload.page_title") }}</h1>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from Device</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_device") }}</h2>
|
||||
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
|
||||
<div
|
||||
id="dropZone"
|
||||
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="File upload area. Drag and drop files here, or press Enter to browse files."
|
||||
aria-label="{{ _('upload.drop_zone_aria') }}"
|
||||
ondragover="handleDragOver(event)"
|
||||
ondragleave="handleDragLeave(event)"
|
||||
>
|
||||
<p class="text-gray-500 mb-4 hidden sm:block">
|
||||
Drag & drop files or folders here, or click to select files.
|
||||
{{ _("upload.drop_hint_desktop") }}
|
||||
</p>
|
||||
<p class="text-gray-500 mb-4 sm:hidden">
|
||||
Tap to select files or use the camera button below.
|
||||
{{ _("upload.drop_hint_mobile") }}
|
||||
</p>
|
||||
<input
|
||||
id="fileInput"
|
||||
@@ -31,7 +31,7 @@
|
||||
onchange="handleFileSelect(event)"
|
||||
name="files"
|
||||
multiple
|
||||
aria-label="Select files to upload"
|
||||
aria-label="{{ _('upload.select_file') }}"
|
||||
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
|
||||
/>
|
||||
<!-- Browse Files button (all devices) -->
|
||||
@@ -41,11 +41,11 @@
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> Browse Files
|
||||
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> {{ _("upload.browse_button") }}
|
||||
</button>
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
|
||||
<p>Maximum size: 500MB per file • Directories are uploaded recursively</p>
|
||||
<p>{{ _("upload.file_types") }}</p>
|
||||
<p>{{ _("upload.file_size_hint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-camera" aria-hidden="true"></i> Take Photo / Scan Document
|
||||
<i class="fas fa-camera" aria-hidden="true"></i> {{ _("upload.camera_button") }}
|
||||
<input
|
||||
id="cameraInput"
|
||||
type="file"
|
||||
@@ -63,7 +63,7 @@
|
||||
onchange="handleFileSelect(event)"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
aria-label="Take photo or scan document"
|
||||
aria-label="{{ _('upload.camera_button') }}"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -72,40 +72,40 @@
|
||||
|
||||
<!-- URL Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from URL</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_url") }}</h2>
|
||||
<div class="bg-white border border-gray-300 rounded-lg p-4 sm:p-6">
|
||||
<form id="urlUploadForm" class="space-y-4">
|
||||
<div>
|
||||
<label for="urlInput" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
File URL
|
||||
{{ _("upload.url_label") }}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="urlInput"
|
||||
name="url"
|
||||
placeholder="https://example.com/document.pdf"
|
||||
placeholder="{{ _('upload.url_placeholder') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:44px;"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Enter a direct link to a file (PDF, Office documents, or images)
|
||||
{{ _("upload.url_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="urlFilename" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Filename (optional)
|
||||
{{ _("upload.filename_label") }}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="urlFilename"
|
||||
name="filename"
|
||||
placeholder="my-document.pdf"
|
||||
placeholder="{{ _('upload.filename_placeholder') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:44px;"
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Leave empty to use filename from URL
|
||||
{{ _("upload.filename_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -113,7 +113,7 @@
|
||||
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
Download and Process
|
||||
{{ _("upload.download_button") }}
|
||||
</button>
|
||||
</form>
|
||||
<div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocuElevate - Verify Your Email</title>
|
||||
<title>{{ _("auth.verify_email_page_title") }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
||||
@@ -13,7 +13,7 @@
|
||||
<body class="bg-gray-100 min-h-screen flex items-center justify-center py-8">
|
||||
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center" role="main">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
|
||||
<img src="/static/images/logo_writing.svg" alt="{{ _('auth.logo_alt') }}" class="h-16">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
@@ -22,19 +22,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">Check your inbox</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">{{ _("auth.verify_email_heading") }}</h1>
|
||||
<p class="text-gray-600 mb-6">
|
||||
We've sent you a verification email. Please click the link in the email to activate your account.
|
||||
{{ _("auth.verify_email_message") }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mb-8">
|
||||
The link expires in 24 hours. If you don't see the email, check your spam folder.
|
||||
{{ _("auth.verify_email_expiry") }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
x-data="{ email: '', loading: false, message: '', error: '' }"
|
||||
class="border-t border-gray-100 pt-6"
|
||||
>
|
||||
<p class="text-sm text-gray-600 mb-3">Didn't receive it?</p>
|
||||
<p class="text-sm text-gray-600 mb-3">{{ _("auth.verify_email_resend_prompt") }}</p>
|
||||
|
||||
<div x-show="message" x-cloak
|
||||
class="bg-green-100 border-l-4 border-green-500 text-green-700 p-3 mb-4 rounded text-sm"
|
||||
@@ -73,14 +73,14 @@
|
||||
novalidate
|
||||
>
|
||||
<div>
|
||||
<label for="resend-email" class="sr-only">Email address</label>
|
||||
<label for="resend-email" class="sr-only">{{ _("auth.verify_email_resend_label") }}</label>
|
||||
<input
|
||||
type="email" id="resend-email" name="email"
|
||||
x-model="email"
|
||||
placeholder="Enter your email address"
|
||||
placeholder="{{ _('auth.verify_email_resend_placeholder') }}"
|
||||
autocomplete="email"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-500 focus:ring-opacity-50 text-sm"
|
||||
aria-label="Email address for resend"
|
||||
aria-label="{{ _('auth.verify_email_resend_aria_label') }}"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
</div>
|
||||
@@ -90,9 +90,9 @@
|
||||
class="w-full flex justify-center py-2 px-4 border border-indigo-600 rounded-md text-sm font-medium text-indigo-600 hover:bg-indigo-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<span x-show="!loading">Resend verification email</span>
|
||||
<span x-show="!loading">{{ _("auth.verify_email_resend_button") }}</span>
|
||||
<span x-show="loading" x-cloak>
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>Sending…
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>{{ _("common.loading") }}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a href="/login" class="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
Back to sign in
|
||||
{{ _("auth.verify_email_back_sign_in") }}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Езикът беше променен на {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Български",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "L'idioma s'ha canviat a {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Català",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Jazyk byl změněn na {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Čeština",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Sproget blev ændret til {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Dansk",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,929 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Passwort bestätigen",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Anzeigename",
|
||||
"auth.email_label": "E-Mail",
|
||||
"auth.forgot_password": "Passwort vergessen?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Anmelden",
|
||||
"auth.login_title": "Anmelden",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Abmelden",
|
||||
"auth.my_account": "Mein Konto",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Passwort",
|
||||
"auth.profile": "Profil",
|
||||
"auth.remember_me": "Angemeldet bleiben",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Registrieren",
|
||||
"auth.signup_title": "Registrieren",
|
||||
"auth.username_label": "Benutzername",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Aktionen",
|
||||
"common.active": "Aktiv",
|
||||
"common.all": "Alle",
|
||||
"common.back": "Zurück",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.close": "Schließen",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Abgeschlossen",
|
||||
"common.confirm": "Bestätigen",
|
||||
"common.copied": "Kopiert!",
|
||||
"common.copy": "Kopieren",
|
||||
"common.create": "Erstellen",
|
||||
"common.created": "Erstellt",
|
||||
"common.date": "Datum",
|
||||
"common.delete": "Löschen",
|
||||
"common.description": "Beschreibung",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Deaktiviert",
|
||||
"common.download": "Herunterladen",
|
||||
"common.duplicate": "Duplikat",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.enabled": "Aktiviert",
|
||||
"common.error": "Fehler",
|
||||
"common.failed": "Fehlgeschlagen",
|
||||
"common.filter": "Filtern",
|
||||
"common.inactive": "Inaktiv",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Laden...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Weiter",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "Nein",
|
||||
"common.none": "Keine",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Ausstehend",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.previous": "Zurück",
|
||||
"common.processing": "Verarbeitung",
|
||||
"common.refresh": "Aktualisieren",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.retry": "Erneut versuchen",
|
||||
"common.save": "Speichern",
|
||||
"common.search": "Suche",
|
||||
"common.select": "Auswählen",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Größe",
|
||||
"common.status": "Status",
|
||||
"common.submit": "Absenden",
|
||||
"common.success": "Erfolg",
|
||||
"common.tags": "Tags",
|
||||
"common.type": "Typ",
|
||||
"common.updated": "Aktualisiert",
|
||||
"common.upload": "Hochladen",
|
||||
"common.view": "Ansehen",
|
||||
"common.warning": "Warnung",
|
||||
"common.yes": "Ja",
|
||||
"cookie.accept": "Akzeptieren",
|
||||
"cookie.learn_more": "Mehr erfahren",
|
||||
"cookie.message": "Diese Website verwendet Cookies, um Ihr Erlebnis zu verbessern.",
|
||||
"cookie.notice": "DocuElevate verwendet nur essentielle Sitzungscookies, die für die Authentifizierung und den Servicebetrieb erforderlich sind. Es werden keine Tracking- oder Analyse-Cookies verwendet.",
|
||||
"cookie.notice_label": "Cookie-Hinweis",
|
||||
"cookie.policy_link": "Cookie-Richtlinie",
|
||||
"cookie.privacy_link": "Datenschutzhinweis",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Aktive Integrationen",
|
||||
"dashboard.files_this_month": "Dateien diesen Monat",
|
||||
"dashboard.files_today": "Dateien heute",
|
||||
"dashboard.ocr_processed": "OCR verarbeitet",
|
||||
"dashboard.quick_actions": "Schnellaktionen",
|
||||
"dashboard.recent_activity": "Letzte Aktivitäten",
|
||||
"dashboard.storage_targets": "Speicherziele",
|
||||
"dashboard.title": "Übersicht",
|
||||
"dashboard.total_files": "Dateien gesamt",
|
||||
"dashboard.welcome": "Willkommen bei DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Ups, diese Seite konnten wir nicht finden!",
|
||||
"error.404_home": "Zur Startseite",
|
||||
"error.404_message": "Es scheint, als hätte DocuElevate das gesuchte Dokument verlegt. Keine Sorge – wir helfen Ihnen weiter.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Unsere Server haben ein Problem und brauchen einen Moment.",
|
||||
"error.500_heading": "Ups! Etwas ist schiefgelaufen.",
|
||||
"error.500_home": "Zur Startseite",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Zugriff verweigert",
|
||||
"error.forbidden_message": "Sie haben keine Berechtigung, auf diese Seite zuzugreifen.",
|
||||
"error.not_found": "Seite nicht gefunden",
|
||||
"error.not_found_message": "Die gesuchte Seite existiert nicht.",
|
||||
"error.server_error": "Interner Serverfehler",
|
||||
"error.server_error_message": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut.",
|
||||
"error.unauthorized": "Nicht autorisiert",
|
||||
"error.unauthorized_message": "Sie müssen sich anmelden, um auf diese Seite zuzugreifen.",
|
||||
"files.action_delete": "Datei löschen",
|
||||
"files.action_details": "Details anzeigen",
|
||||
"files.action_preview": "Schnellvorschau",
|
||||
"files.bulk_clear_selection": "Auswahl aufheben",
|
||||
"files.bulk_cloud_ocr": "Cloud-OCR erneut ausführen",
|
||||
"files.bulk_delete": "Ausgewählte löschen",
|
||||
"files.bulk_download": "Als ZIP herunterladen",
|
||||
"files.bulk_reprocess": "Ausgewählte erneut verarbeiten",
|
||||
"files.delete_modal_cancel": "Abbrechen",
|
||||
"files.delete_modal_confirm": "Löschen",
|
||||
"files.delete_modal_message": "Sind Sie sicher, dass Sie diese Datei löschen möchten?",
|
||||
"files.delete_modal_title": "Löschung bestätigen",
|
||||
"files.document_title": "Dokumenttitel",
|
||||
"files.drop_overlay_hint": "Unterstützt PDF, Office-Dokumente, Bilder, HTML, Markdown und mehr",
|
||||
"files.drop_overlay_title": "Dateien oder Ordner zum Hochladen hier ablegen",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "Dateigröße",
|
||||
"files.filename": "Dateiname",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "Alle Anbieter",
|
||||
"files.filter_all_statuses": "Alle Status",
|
||||
"files.filter_all_types": "Alle Typen",
|
||||
"files.filter_apply": "Filter anwenden",
|
||||
"files.filter_clear": "Zurücksetzen",
|
||||
"files.filter_date_from": "Datum von",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Datum bis",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME-Typ",
|
||||
"files.filter_ocr_all": "Alle Dateien",
|
||||
"files.filter_ocr_good": "Gute Qualität",
|
||||
"files.filter_ocr_poor": "Schlechte Qualität",
|
||||
"files.filter_ocr_quality": "OCR-Qualität",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Noch nicht bewertet",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Dateinamen eingeben...",
|
||||
"files.filter_storage_provider": "Speicheranbieter",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "z.B. Rechnung,Amazon",
|
||||
"files.fulltext_search_label": "Volltextsuche",
|
||||
"files.fulltext_search_placeholder": "Dokumentinhalt, Absender, Tags, Typ durchsuchen...",
|
||||
"files.no_files": "Keine Dateien gefunden",
|
||||
"files.ocr_status": "OCR-Status",
|
||||
"files.page_title": "Dateiübersicht",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "Erste",
|
||||
"files.pagination_last": "Letzte",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Nächste",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Vorherige",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Vorschau schließen",
|
||||
"files.preview_modal_title": "Vorschau",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "Warteschlange ansehen",
|
||||
"files.saved_searches_empty": "Noch keine gespeicherten Suchen",
|
||||
"files.saved_searches_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"files.saved_searches_label": "Gespeicherte Suchen",
|
||||
"files.saved_searches_save": "Aktuelle speichern",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "Keine Ergebnisse gefunden.",
|
||||
"files.search_results_title": "Suchergebnisse",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Aktionen",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Erstellt am",
|
||||
"files.table_empty": "Keine Dateien gefunden",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME-Typ",
|
||||
"files.table_original_filename": "Originaler Dateiname",
|
||||
"files.table_select_all": "Alle Dateien auf dieser Seite auswählen",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Dateien",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Dateien hochladen",
|
||||
"files.uploaded": "Hochgeladen",
|
||||
"footer.about": "Über uns",
|
||||
"footer.attribution": "Namensnennung",
|
||||
"footer.attributions": "Quellenangaben",
|
||||
"footer.cookies": "Cookie-Richtlinie",
|
||||
"footer.copyright": "© {year} DocuElevate",
|
||||
"footer.imprint": "Impressum",
|
||||
"footer.license": "Lizenz",
|
||||
"footer.navigation": "Fußzeilennavigation",
|
||||
"footer.privacy": "Datenschutz",
|
||||
"footer.terms": "Nutzungsbedingungen",
|
||||
"footer.version": "Version",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-verknüpft. Dateien landen in Ihrem gewählten Ordner.",
|
||||
"help.destinations_email": "E-Mail-Weiterleitung",
|
||||
"help.destinations_email_desc": "Verarbeitete Dateien als SMTP-Anhänge gesendet.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Dienstkonto oder OAuth. Unterstützt geteilte Laufwerke.",
|
||||
"help.destinations_heading": "Ziele – Wohin die Dokumente gehen",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Selbstgehosteter Cloud-Speicher über WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API-Integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Dokumente direkt in Paperless zur Archivierung übertragen.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Jeder S3-kompatible Bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Sichere Dateiübertragung auf beliebige Server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "Metadaten per POST an einen externen Endpunkt senden.",
|
||||
"help.documentation": "Dokumentation",
|
||||
"help.faq": "Häufig gestellte Fragen",
|
||||
"help.faq_1_a": "Navigieren Sie zur Upload-Seite, ziehen Sie Ihre Dateien per Drag-and-Drop oder klicken Sie auf Datei auswählen. DocuElevate konvertiert Bilder und Office-Dokumente in PDF, führt OCR durch und extrahiert Metadaten automatisch.",
|
||||
"help.faq_1_q": "Wie lade ich Dokumente hoch?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF und HTML. Nicht-PDF-Dateien werden vor der Verarbeitung automatisch in PDF konvertiert.",
|
||||
"help.faq_2_q": "Welche Dateiformate werden unterstützt?",
|
||||
"help.faq_3_a": "Ja. Gehen Sie zu E-Mail-Import, fügen Sie ein IMAP-Konto hinzu, und DocuElevate prüft auf neue Nachrichten und verarbeitet Anhänge automatisch.",
|
||||
"help.faq_3_q": "Kann ich Dokumente per E-Mail importieren?",
|
||||
"help.faq_4_a": "Pipelines ermöglichen es Ihnen, Verarbeitungsschritte zu verketten – OCR, KI-Extraktion, Formatkonvertierung – und das Ergebnis an ein oder mehrere Ziele weiterzuleiten. Erstellen und verwalten Sie diese auf der Pipelines-Seite.",
|
||||
"help.faq_4_q": "Wie funktionieren Verarbeitungs-Pipelines?",
|
||||
"help.faq_5_a": "DocuElevate verschlüsselt Anmeldedaten im Ruhezustand, kommuniziert über TLS und speichert Ihre Dokumente nie länger als nötig. Weitere Details finden Sie in der Datenschutzerklärung.",
|
||||
"help.faq_5_q": "Sind meine Daten sicher?",
|
||||
"help.faq_heading": "Häufig gestellte Fragen",
|
||||
"help.getting_started": "Erste Schritte",
|
||||
"help.heading": "Hilfezentrum",
|
||||
"help.page_title": "Hilfezentrum",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Schnellstart",
|
||||
"help.quickstart_storage": "Speicher verbinden",
|
||||
"help.quickstart_storage_desc": "Gehen Sie zu Einstellungen und verknüpfen Sie Ihre Cloud-Konten. Verarbeitete Dokumente werden automatisch an jedes konfigurierte Ziel weitergeleitet.",
|
||||
"help.quickstart_upload": "Dokumente hochladen",
|
||||
"help.quickstart_upload_desc": "Ziehen Sie Dateien auf die Upload-Seite oder klicken Sie auf Datei auswählen. DocuElevate konvertiert Bilder und Office-Dokumente in PDF, führt OCR durch und extrahiert Metadaten automatisch.",
|
||||
"help.quickstart_workflows": "Arbeitsabläufe automatisieren",
|
||||
"help.quickstart_workflows_desc": "Erstellen Sie Pipelines, um mehrstufige Verarbeitungs- und Weiterleitungsregeln zu definieren. Kombinieren Sie OCR, KI-Extraktion, Formatkonvertierung und Zustellung in einem einzigen Ablauf.",
|
||||
"help.sources_email_ingestion": "E-Mail-Import (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Leiten Sie Dokumente an ein dediziertes Postfach weiter. Unter E-Mail-Import fügen Sie ein oder mehrere IMAP-Konten hinzu. DocuElevate prüft auf neue Nachrichten und verarbeitet Anhänge automatisch.",
|
||||
"help.sources_heading": "Quellen – Dokumente einbringen",
|
||||
"help.sources_rest_api": "REST-API",
|
||||
"help.sources_rest_api_desc": "Integrieren Sie programmgesteuert, indem Sie Dateien an /api/upload senden. Ideal für Skripte, überwachte Ordner, Scanner oder Drittanbieter-Tools wie Zapier und n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobil",
|
||||
"help.sources_scanner_desc": "Richten Sie Netzwerkscanner auf den Upload-Endpunkt von DocuElevate oder verwenden Sie eine mobile Scan-App, die benutzerdefinierte HTTP-Ziele unterstützt.",
|
||||
"help.sources_web_upload": "Web-Upload",
|
||||
"help.sources_web_upload_desc": "Der schnellste Weg, um loszulegen. Öffnen Sie die Upload-Seite, legen Sie eine oder mehrere Dateien ab, und DocuElevate kümmert sich um den Rest. Unterstützte Formate sind PDF, JPEG, PNG, TIFF, DOCX, XLSX und mehr.",
|
||||
"help.subheading": "Alles, was Sie brauchen, um DocuElevate optimal zu nutzen. Durchsuchen Sie die Themen unten oder suchen Sie nach dem, was Sie brauchen.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Wenden Sie sich an Ihren Administrator für Support-Informationen.",
|
||||
"help.support_description": "Können Sie nicht finden, was Sie suchen? Unser Support-Team hilft Ihnen gerne weiter.",
|
||||
"help.support_heading": "Support kontaktieren",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Hilfecenter",
|
||||
"help.workflows_creating": "Eine Pipeline erstellen",
|
||||
"help.workflows_definition": "Eine Pipeline ist eine Reihe von Verarbeitungsschritten, die automatisch ausgeführt werden, wenn ein Dokument aufgenommen wird. Jeder Schritt kann das Dokument transformieren, anreichern oder weiterleiten.",
|
||||
"help.workflows_heading": "Arbeitsabläufe & Pipelines",
|
||||
"help.workflows_step_1": "In PDF konvertieren",
|
||||
"help.workflows_step_1_create": "Gehen Sie im Hauptmenü zu Pipelines.",
|
||||
"help.workflows_step_2": "OCR – Text extrahieren",
|
||||
"help.workflows_step_2_create": "Klicken Sie auf Neue Pipeline und geben Sie ihr einen Namen.",
|
||||
"help.workflows_step_3": "KI-Metadatenextraktion",
|
||||
"help.workflows_step_3_create": "Fügen Sie die benötigten Verarbeitungsschritte hinzu.",
|
||||
"help.workflows_step_4": "An ein oder mehrere Ziele liefern",
|
||||
"help.workflows_step_4_create": "Wählen Sie ein oder mehrere Zustellungsziele.",
|
||||
"help.workflows_step_5_create": "Speichern – neue Dokumente werden automatisch durch diese Pipeline verarbeitet.",
|
||||
"help.workflows_typical_steps": "Typische Schritte",
|
||||
"help.workflows_what_is": "Was ist eine Pipeline?",
|
||||
"index.badge_intelligent": "Intelligente Dokumentenverarbeitung",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud-Speicher: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "E-Mail- & URL-basierte Dokumentenaufnahme",
|
||||
"index.capabilities_ocr": "OCR & Metadatenextraktion mit KI",
|
||||
"index.capabilities_paperless": "Paperless-ngx-Integration für Dokumentenverwaltung",
|
||||
"index.capabilities_title": "Funktionen",
|
||||
"index.capabilities_workflows": "Automatisierte Klassifizierung & Routing-Workflows",
|
||||
"index.cta_description": "Schließen Sie sich Teams an, die ihre Dokumentenverarbeitung bereits mit DocuElevate automatisieren.",
|
||||
"index.cta_heading": "Bereit, Ihren Dokumenten-Workflow zu verbessern?",
|
||||
"index.cta_pricing": "Preise ansehen",
|
||||
"index.cta_signup": "Kostenloses Konto erstellen",
|
||||
"index.dashboard_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "KI-Metadatenextraktion",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini und andere KI-Anbieter klassifizieren Dokumente und extrahieren wichtige Felder wie Daten, Beträge und Betreffzeilen.",
|
||||
"index.feature_cloud": "Multi-Cloud-Speicher",
|
||||
"index.feature_cloud_desc": "Leiten Sie verarbeitete Dateien an Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP und mehr weiter.",
|
||||
"index.feature_email": "E-Mail- & IMAP-Import",
|
||||
"index.feature_email_desc": "Ziehen Sie Dokumente automatisch aus Gmail oder jedem IMAP-Postfach – keine manuellen Uploads nötig.",
|
||||
"index.feature_ocr": "OCR & Texterkennung",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence konvertiert gescannte PDFs und Bilder automatisch in vollständig durchsuchbaren Text.",
|
||||
"index.feature_pipelines": "Benutzerdefinierte Pipelines",
|
||||
"index.feature_pipelines_desc": "Erstellen Sie Verarbeitungs-Pipelines mit konfigurierbaren Schritten – OCR, KI-Extraktion, Formatkonvertierung und Speicher-Routing in beliebiger Reihenfolge.",
|
||||
"index.feature_search": "Volltextsuche",
|
||||
"index.feature_search_desc": "Finden Sie sofort jedes Dokument nach Inhalt, Metadaten oder Tags in Ihrem gesamten Archiv.",
|
||||
"index.feature_section_title": "Alles, was Sie für intelligente Dokumenten-Workflows brauchen",
|
||||
"index.getting_started": "Erste Schritte",
|
||||
"index.getting_started_1": "Integrationen über Systemstatus konfigurieren",
|
||||
"index.getting_started_2": "Erstes Dokument hochladen",
|
||||
"index.getting_started_3": "Ergebnisse in Dateien überprüfen",
|
||||
"index.getting_started_learn": "Mehr über DocuElevate erfahren",
|
||||
"index.hero_description": "DocuElevate nimmt Ihre Dokumente auf, führt OCR durch, extrahiert Metadaten mit KI und leitet Dateien an Dropbox, Google Drive, OneDrive, S3, Nextcloud und mehr weiter – alles in einer nahtlosen Pipeline.",
|
||||
"index.hero_heading": "Vom Hochladen zur Erkenntnis – automatisch.",
|
||||
"index.hero_login": "Anmelden",
|
||||
"index.hero_pricing": "Tarife & Preise ansehen",
|
||||
"index.hero_signup": "Kostenlos starten",
|
||||
"index.integrations_active": "Aktive Integrationen",
|
||||
"index.integrations_storage": "Speicherziele",
|
||||
"index.integrations_title": "Integrationen",
|
||||
"index.integrations_view_status": "Systemstatus anzeigen",
|
||||
"index.page_title_dashboard": "Übersicht",
|
||||
"index.page_title_public": "Intelligente Dokumentenverarbeitung",
|
||||
"index.platform_overview": "Plattformübersicht",
|
||||
"index.quick_actions": "Schnellaktionen",
|
||||
"index.quick_documents": "Meine Dokumente",
|
||||
"index.quick_documents_desc": "Ihre verarbeiteten Dateien durchsuchen",
|
||||
"index.quick_search": "Suche",
|
||||
"index.quick_search_desc": "Volltextsuche über Dokumente",
|
||||
"index.quick_subscription": "Mein Abonnement",
|
||||
"index.quick_subscription_desc": "Tarif & Nutzungsdetails anzeigen",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Dokument hochladen",
|
||||
"index.quick_upload_desc": "Eine neue Datei verarbeiten",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Aktive Benutzer",
|
||||
"index.stat_files_month": "Dateien diesen Monat",
|
||||
"index.stat_files_today": "Dateien heute",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Dateien gesamt",
|
||||
"index.tier_plan": "Tarif",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "Alle Details ansehen",
|
||||
"index.upgrade_daily_limits": "Höhere tägliche & monatliche Limits",
|
||||
"index.upgrade_description": "Mehr Dokumente, mehr Ziele und Prioritäts-Support freischalten.",
|
||||
"index.upgrade_destinations": "Mehr Speicherziele",
|
||||
"index.upgrade_ocr_pages": "Mehr OCR-Seiten",
|
||||
"index.upgrade_plan": "Tarif upgraden",
|
||||
"index.upgrade_view_pricing": "Tarife & Preise ansehen",
|
||||
"index.usage_lifetime": "Dateien gesamt",
|
||||
"index.usage_month": "Dateien diesen Monat",
|
||||
"index.usage_my_usage": "Meine Nutzung",
|
||||
"index.usage_today": "Dateien heute",
|
||||
"index.usage_unlimited": "Unbegrenzt",
|
||||
"integrations.configure": "Konfigurieren",
|
||||
"integrations.connect": "Verbinden",
|
||||
"integrations.connected": "Verbunden",
|
||||
"integrations.disconnect": "Trennen",
|
||||
"integrations.empty_state": "Keine Integrationen konfiguriert",
|
||||
"integrations.folder_label": "Ordner",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP-Einstellungen",
|
||||
"integrations.not_connected": "Nicht verbunden",
|
||||
"integrations.page_title": "Integrationen",
|
||||
"integrations.password_label": "Passwort",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrationen",
|
||||
"integrations.username_label": "Benutzername",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.change_success": "Sprache geändert zu {language}",
|
||||
"language.changed": "Sprache geändert zu {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "Englisch",
|
||||
"language.es": "Spanisch",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Französisch",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italienisch",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Niederländisch",
|
||||
"language.pl": "Polnisch",
|
||||
"language.pt": "Portugiesisch",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Russisch",
|
||||
"language.selector": "Sprache",
|
||||
"language.selector_label": "Sprache wählen",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "Chinesisch",
|
||||
"nav.about": "Über uns",
|
||||
"nav.admin": "Administration",
|
||||
"nav.admin.audit_logs": "Prüfprotokolle",
|
||||
"nav.admin.backups": "Sicherungen",
|
||||
"nav.admin.plans": "Tarife",
|
||||
"nav.admin.scheduled_jobs": "Geplante Aufgaben",
|
||||
"nav.admin.users": "Benutzer",
|
||||
"nav.admin_actions": "Admin-Aktionen",
|
||||
"nav.admin_menu": "Admin-Menü",
|
||||
"nav.api_docs": "API-Dokumentation",
|
||||
"nav.api_tokens": "API-Token",
|
||||
"nav.backup_restore": "Sicherung & Wiederherstellung",
|
||||
"nav.credentials": "Zugangsdaten",
|
||||
"nav.dark_mode": "Dunkelmodus",
|
||||
"nav.dashboard": "Übersicht",
|
||||
"nav.developer_docs": "Entwicklerdokumentation",
|
||||
"nav.duplicates": "Duplikate",
|
||||
"nav.file_manager": "Dateimanager",
|
||||
"nav.files": "Dateien",
|
||||
"nav.help": "Hilfe",
|
||||
"nav.help_center": "Hilfecenter",
|
||||
"nav.imap": "E-Mail-Import",
|
||||
"nav.integrations": "Integrationen",
|
||||
"nav.light_mode": "Hellmodus",
|
||||
"nav.login": "Anmelden",
|
||||
"nav.logout": "Abmelden",
|
||||
"nav.main_navigation": "Hauptnavigation",
|
||||
"nav.notifications": "Benachrichtigungen",
|
||||
"nav.open_main_menu": "Hauptmenü öffnen",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan-Designer",
|
||||
"nav.pricing": "Preise",
|
||||
"nav.profile": "Profil",
|
||||
"nav.queue": "Warteschlange",
|
||||
"nav.queue_monitor": "Warteschlangen-Monitor",
|
||||
"nav.scheduled_jobs": "Geplante Aufgaben",
|
||||
"nav.search": "Suche",
|
||||
"nav.settings": "Einstellungen",
|
||||
"nav.shared_links": "Geteilte Links",
|
||||
"nav.signup": "Registrieren",
|
||||
"nav.similarity": "Ähnlichkeit",
|
||||
"nav.skip_to_content": "Zum Hauptinhalt springen",
|
||||
"nav.status": "Systemstatus",
|
||||
"nav.subscription": "Abonnement",
|
||||
"nav.toggle_dark_mode": "Dunkelmodus umschalten",
|
||||
"nav.toggle_nav": "Navigationsmenü umschalten",
|
||||
"nav.upload": "Hochladen",
|
||||
"nav.users": "Benutzer",
|
||||
"nav.version": "Versionsinformationen",
|
||||
"notifications.filter_all": "Alle",
|
||||
"notifications.filter_read": "Nur gelesene",
|
||||
"notifications.filter_unread": "Nur ungelesene",
|
||||
"notifications.manage_desc": "Verwalten Sie Ihren Posteingang, Ziele und Ereignispräferenzen",
|
||||
"notifications.mark_all_read": "Alle als gelesen markieren",
|
||||
"notifications.mark_all_read_btn": "Alle als gelesen markieren",
|
||||
"notifications.mark_read": "Als gelesen markieren",
|
||||
"notifications.no_notifications": "Keine Benachrichtigungen",
|
||||
"notifications.page_title": "Benachrichtigungen",
|
||||
"notifications.tab_inbox": "Posteingang",
|
||||
"notifications.tab_settings": "Einstellungen",
|
||||
"notifications.title": "Benachrichtigungen",
|
||||
"notifications.unread_count": "{count} ungelesene Benachrichtigungen",
|
||||
"pipelines.active_label": "Aktiv",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Pipeline erstellen",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Standard",
|
||||
"pipelines.description_label": "Beschreibung",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Deaktiviert",
|
||||
"pipelines.edit": "Pipeline bearbeiten",
|
||||
"pipelines.empty_state": "Noch keine Pipelines",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Aktiviert",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inaktiv",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Verarbeitungs-Pipelines",
|
||||
"pipelines.set_default": "Als meine Standard-Pipeline festlegen",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Verarbeitungs-Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Suchen",
|
||||
"search.error_message": "Suche ist vorübergehend nicht verfügbar. Bitte versuchen Sie es gleich erneut.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Filter zurücksetzen",
|
||||
"search.filter_date_from": "Datum von",
|
||||
"search.filter_date_to": "Datum bis",
|
||||
"search.filter_document_type": "Dokumenttyp",
|
||||
"search.filter_document_type_placeholder": "z.B. Rechnung",
|
||||
"search.filter_language": "Sprache",
|
||||
"search.filter_language_placeholder": "z.B. de",
|
||||
"search.filter_sender": "Absender",
|
||||
"search.filter_sender_placeholder": "z.B. ACME GmbH",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "z.B. Amazon",
|
||||
"search.filter_text_quality": "Textqualität",
|
||||
"search.filter_text_quality_all": "Alle",
|
||||
"search.filter_text_quality_high": "Hoch",
|
||||
"search.filter_text_quality_low": "Niedrig",
|
||||
"search.filter_text_quality_medium": "Mittel",
|
||||
"search.filter_text_quality_no_text": "Kein Text",
|
||||
"search.heading": "Dokumentensuche",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Dokumente nach Inhalt, Absender, Tags, Typ suchen...",
|
||||
"search.loading_indicator": "Suche läuft…",
|
||||
"search.no_results": "Keine Ergebnisse gefunden",
|
||||
"search.page_title": "Dokumente suchen",
|
||||
"search.placeholder": "Nach Dateiname, Inhalt, Tags suchen...",
|
||||
"search.result_empty": "Keine Dokumente gefunden, die Ihrer Suche entsprechen.",
|
||||
"search.results_count": "{count} Ergebnisse gefunden",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Aktuelle speichern",
|
||||
"search.saved_empty": "Noch keine gespeicherten Suchen",
|
||||
"search.saved_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"search.saved_label": "Gespeicherte Suchen",
|
||||
"search.saved_loading": "Laden...",
|
||||
"search.title": "Dokumente suchen",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Möchten Sie diese Einstellung wirklich zurücksetzen?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Einstellung konnte nicht gespeichert werden",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Einstellung erfolgreich gespeichert",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Einstellungen",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App-Version",
|
||||
"status.build_date": "Build-Datum",
|
||||
"status.container_id": "Container-ID",
|
||||
"status.git_commit": "Git-Commit",
|
||||
"status.last_check": "Letzte Prüfung",
|
||||
"status.page_title": "Systemstatus",
|
||||
"status.setting_label": "Einstellung",
|
||||
"status.value_label": "Wert",
|
||||
"upload.browse_button": "Dateien durchsuchen",
|
||||
"upload.button_processing": "Verarbeitung...",
|
||||
"upload.camera_button": "Foto aufnehmen / Dokument scannen",
|
||||
"upload.download_button": "Herunterladen und verarbeiten",
|
||||
"upload.downloading": "Datei wird von URL heruntergeladen...",
|
||||
"upload.drag_drop": "Dateien hierher ziehen oder zum Durchsuchen klicken",
|
||||
"upload.drop_hint_desktop": "Dateien oder Ordner hierher ziehen oder klicken, um Dateien auszuwählen.",
|
||||
"upload.drop_hint_mobile": "Tippen Sie, um Dateien auszuwählen, oder verwenden Sie die Kamera-Schaltfläche unten.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload fehlgeschlagen",
|
||||
"upload.error_invalid_url": "Ungültiges URL-Format",
|
||||
"upload.error_url_required": "Bitte geben Sie eine URL ein",
|
||||
"upload.file_size_hint": "Maximale Größe: 500 MB pro Datei",
|
||||
"upload.file_types": "Erlaubte Typen: PDF, Office-Dokumente (Word, Excel, PowerPoint usw.), Bilder",
|
||||
"upload.filename_description": "Leer lassen, um den Dateinamen aus der URL zu verwenden",
|
||||
"upload.filename_label": "Dateiname (optional)",
|
||||
"upload.filename_placeholder": "mein-dokument.pdf",
|
||||
"upload.max_size": "Maximale Dateigröße: {size}",
|
||||
"upload.page_title": "Dateien hochladen",
|
||||
"upload.section_device": "Vom Gerät hochladen",
|
||||
"upload.section_url": "Von URL hochladen",
|
||||
"upload.select_file": "Datei auswählen",
|
||||
"upload.success": "Datei erfolgreich hochgeladen",
|
||||
"upload.title": "Dokument hochladen",
|
||||
"upload.uploading": "Wird hochgeladen...",
|
||||
"upload.url_description": "Geben Sie einen direkten Link zu einer Datei ein (PDF, Office-Dokumente oder Bilder)",
|
||||
"upload.url_label": "Datei-URL",
|
||||
"upload.url_placeholder": "https://beispiel.de/dokument.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Η γλώσσα άλλαξε σε {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Ελληνικά",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Upload",
|
||||
"nav.files": "Files",
|
||||
"nav.search": "Search",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.help": "Help",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Settings",
|
||||
"nav.users": "Users",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.help_center": "Help Center",
|
||||
"auth.login": "Log In",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.profile": "Profile",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.terms": "Terms",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "License",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.version": "Version {version}",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"common.save": "Save",
|
||||
"common.cancel": "Cancel",
|
||||
"common.delete": "Delete",
|
||||
"common.edit": "Edit",
|
||||
"common.close": "Close",
|
||||
"common.confirm": "Confirm",
|
||||
"common.back": "Back",
|
||||
"common.next": "Next",
|
||||
"common.loading": "Loading...",
|
||||
"common.error": "Error",
|
||||
"common.success": "Success",
|
||||
"common.warning": "Warning",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No",
|
||||
"common.search": "Search",
|
||||
"common.filter": "Filter",
|
||||
"common.reset": "Reset",
|
||||
"common.refresh": "Refresh",
|
||||
"common.download": "Download",
|
||||
"common.actions": "Actions",
|
||||
"common.details": "Details",
|
||||
"common.name": "Name",
|
||||
"common.description": "Description",
|
||||
"common.type": "Type",
|
||||
"common.status": "Status",
|
||||
"common.date": "Date",
|
||||
"common.size": "Size",
|
||||
"common.created": "Created",
|
||||
"common.updated": "Updated",
|
||||
"common.enabled": "Enabled",
|
||||
"common.disabled": "Disabled",
|
||||
"common.active": "Active",
|
||||
"common.inactive": "Inactive",
|
||||
"common.all": "All",
|
||||
"common.none": "None",
|
||||
"common.select": "Select",
|
||||
"common.upload": "Upload",
|
||||
"common.processing": "Processing",
|
||||
"common.completed": "Completed",
|
||||
"common.failed": "Failed",
|
||||
"common.pending": "Pending",
|
||||
"common.retry": "Retry",
|
||||
"common.view": "View",
|
||||
"common.copy": "Copy",
|
||||
"common.copied": "Copied!",
|
||||
"language.selector": "Language",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Language changed to {language}",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"files.title": "Files",
|
||||
"files.no_files": "No files found",
|
||||
"files.filename": "Filename",
|
||||
"files.document_title": "Document Title",
|
||||
"files.uploaded": "Uploaded",
|
||||
"files.file_size": "File Size",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.tags": "Tags",
|
||||
"search.title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.no_results": "No results found",
|
||||
"search.results_count": "{count} results found",
|
||||
"settings.title": "Settings",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.configure": "Configure",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"help.title": "Help Center",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.documentation": "Documentation",
|
||||
"help.support": "Support",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"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 – folders are processed recursively",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"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 (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.",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.table_id": "ID",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_empty": "No files found",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.action_details": "View details",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_last": "Last",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.button": "Search",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"help.page_title": "Help Center",
|
||||
"help.heading": "Help Center",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_home": "Return Home",
|
||||
"error.500_code": "500",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_home": "Go Home",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.active_label": "Active",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.username_label": "Username",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"status.page_title": "System Status",
|
||||
"status.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.last_check": "Last Check",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.email_label": "Email",
|
||||
"auth.password_label": "Password",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.username_label": "Username",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"language.nb": "Norsk",
|
||||
"language.da": "Dansk",
|
||||
"language.sv": "Svenska",
|
||||
"language.fi": "Suomi",
|
||||
"language.is": "Íslenska",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hu": "Magyar",
|
||||
"language.cs": "Čeština",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.ro": "Română",
|
||||
"language.bg": "Български",
|
||||
"language.uk": "Українська",
|
||||
"language.tr": "Türkçe",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.et": "Eesti",
|
||||
"language.lv": "Latviešu",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.ca": "Català",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.create_account": "Create account",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.title": "Audit Logs",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.prev": "Prev",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.next": "Next",
|
||||
"audit.critical": "Critical",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.no_data": "No data",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.subtitle": "Pairs of documents with high semantic similarity, ranked by score.",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.trigger_now": "trigger it now",
|
||||
"similarity.trigger_aria": "Trigger embedding computation for all files missing embeddings",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.records_label": "records",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_type": "Type",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.files_link": "Files",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.label_label": "Label",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_views": "Views",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_active": "Active",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.button_upload": "Upload",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.next_page": "Next page",
|
||||
"common.page": "Page",
|
||||
"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 —",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"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 you’re a small startup or a large enterprise.",
|
||||
"about.story_p2": "We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama, OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.legal_license": "License Information",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.sign_in_with": "Sign in with"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Iniciar sesión",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Cerrar sesión",
|
||||
"auth.my_account": "Mi cuenta",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Perfil",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Registrarse",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Acciones",
|
||||
"common.active": "Activo",
|
||||
"common.all": "Todo",
|
||||
"common.back": "Atrás",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.close": "Cerrar",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completado",
|
||||
"common.confirm": "Confirmar",
|
||||
"common.copied": "¡Copiado!",
|
||||
"common.copy": "Copiar",
|
||||
"common.created": "Creado",
|
||||
"common.date": "Fecha",
|
||||
"common.delete": "Eliminar",
|
||||
"common.description": "Descripción",
|
||||
"common.details": "Detalles",
|
||||
"common.disabled": "Deshabilitado",
|
||||
"common.download": "Descargar",
|
||||
"common.edit": "Editar",
|
||||
"common.enabled": "Habilitado",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Fallido",
|
||||
"common.filter": "Filtrar",
|
||||
"common.inactive": "Inactivo",
|
||||
"common.info": "Información",
|
||||
"common.loading": "Cargando...",
|
||||
"common.name": "Nombre",
|
||||
"common.next": "Siguiente",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "Ninguno",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pendiente",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Procesando",
|
||||
"common.refresh": "Actualizar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.retry": "Reintentar",
|
||||
"common.save": "Guardar",
|
||||
"common.search": "Buscar",
|
||||
"common.select": "Seleccionar",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Tamaño",
|
||||
"common.status": "Estado",
|
||||
"common.success": "Éxito",
|
||||
"common.type": "Tipo",
|
||||
"common.updated": "Actualizado",
|
||||
"common.upload": "Subir",
|
||||
"common.view": "Ver",
|
||||
"common.warning": "Advertencia",
|
||||
"common.yes": "Sí",
|
||||
"cookie.accept": "Entendido",
|
||||
"cookie.notice": "DocuElevate utiliza solo cookies de sesión esenciales necesarias para la autenticación y el funcionamiento del servicio. No se utilizan cookies de seguimiento ni analíticas.",
|
||||
"cookie.notice_label": "Aviso de cookies",
|
||||
"cookie.policy_link": "Política de cookies",
|
||||
"cookie.privacy_link": "Aviso de privacidad",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Integraciones activas",
|
||||
"dashboard.files_this_month": "Archivos este mes",
|
||||
"dashboard.files_today": "Archivos hoy",
|
||||
"dashboard.ocr_processed": "OCR procesados",
|
||||
"dashboard.quick_actions": "Acciones rápidas",
|
||||
"dashboard.recent_activity": "Actividad reciente",
|
||||
"dashboard.storage_targets": "Destinos de almacenamiento",
|
||||
"dashboard.title": "Panel",
|
||||
"dashboard.total_files": "Total de archivos",
|
||||
"dashboard.welcome": "Bienvenido a DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Prohibido",
|
||||
"error.forbidden_message": "No tiene permiso para acceder a esta página.",
|
||||
"error.not_found": "Página no encontrada",
|
||||
"error.not_found_message": "La página que busca no existe.",
|
||||
"error.server_error": "Error interno del servidor",
|
||||
"error.server_error_message": "Algo salió mal. Inténtelo de nuevo más tarde.",
|
||||
"error.unauthorized": "No autorizado",
|
||||
"error.unauthorized_message": "Debe iniciar sesión para acceder a esta página.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Título del documento",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "Tamaño del archivo",
|
||||
"files.filename": "Nombre del archivo",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No se encontraron archivos",
|
||||
"files.ocr_status": "Estado OCR",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Etiquetas",
|
||||
"files.title": "Archivos",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Subido",
|
||||
"footer.attributions": "Atribuciones",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Aviso legal",
|
||||
"footer.license": "Licencia",
|
||||
"footer.navigation": "Navegación del pie de página",
|
||||
"footer.privacy": "Privacidad",
|
||||
"footer.terms": "Términos",
|
||||
"footer.version": "Versión {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentación",
|
||||
"help.faq": "Preguntas frecuentes",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Primeros pasos",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Soporte",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Centro de ayuda",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configurar",
|
||||
"integrations.connect": "Conectar",
|
||||
"integrations.connected": "Conectado",
|
||||
"integrations.disconnect": "Desconectar",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "No conectado",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integraciones",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Idioma cambiado a {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Idioma",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "Acerca de",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Acciones de administración",
|
||||
"nav.admin_menu": "Menú de administración",
|
||||
"nav.api_docs": "Documentación API",
|
||||
"nav.backup_restore": "Copia de seguridad y restauración",
|
||||
"nav.credentials": "Credenciales",
|
||||
"nav.dark_mode": "Modo oscuro",
|
||||
"nav.dashboard": "Panel",
|
||||
"nav.developer_docs": "Documentación para desarrolladores",
|
||||
"nav.duplicates": "Duplicados",
|
||||
"nav.file_manager": "Gestor de archivos",
|
||||
"nav.files": "Archivos",
|
||||
"nav.help": "Ayuda",
|
||||
"nav.help_center": "Centro de ayuda",
|
||||
"nav.integrations": "Integraciones",
|
||||
"nav.light_mode": "Modo claro",
|
||||
"nav.main_navigation": "Navegación principal",
|
||||
"nav.notifications": "Notificaciones",
|
||||
"nav.open_main_menu": "Abrir menú principal",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Diseñador de planes",
|
||||
"nav.pricing": "Precios",
|
||||
"nav.queue_monitor": "Monitor de cola",
|
||||
"nav.scheduled_jobs": "Tareas programadas",
|
||||
"nav.search": "Buscar",
|
||||
"nav.settings": "Configuración",
|
||||
"nav.similarity": "Similitud",
|
||||
"nav.skip_to_content": "Ir al contenido principal",
|
||||
"nav.status": "Estado",
|
||||
"nav.toggle_dark_mode": "Alternar modo oscuro",
|
||||
"nav.toggle_nav": "Alternar menú de navegación",
|
||||
"nav.upload": "Subir",
|
||||
"nav.users": "Usuarios",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Marcar todo como leído",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Marcar como leído",
|
||||
"notifications.no_notifications": "Sin notificaciones",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notificaciones",
|
||||
"notifications.unread_count": "{count} notificaciones no leídas",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Crear pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Editar pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Pipelines de procesamiento",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No se encontraron resultados",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Buscar por nombre, contenido, etiquetas...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} resultados encontrados",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Buscar documentos",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "¿Está seguro de que desea restablecer esta configuración?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Error al guardar la configuración",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Configuración guardada con éxito",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Configuración",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Arrastre archivos aquí o haga clic para buscar",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Error al subir",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Tamaño máximo del archivo: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Seleccionar archivo",
|
||||
"upload.success": "Archivo subido con éxito",
|
||||
"upload.title": "Subir documento",
|
||||
"upload.uploading": "Subiendo...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Keel muudeti keelele {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Eesti",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Kieli vaihdettiin kieleen {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Suomi",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Se connecter",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Se déconnecter",
|
||||
"auth.my_account": "Mon compte",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profil",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "S'inscrire",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Actif",
|
||||
"common.all": "Tout",
|
||||
"common.back": "Retour",
|
||||
"common.cancel": "Annuler",
|
||||
"common.close": "Fermer",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Terminé",
|
||||
"common.confirm": "Confirmer",
|
||||
"common.copied": "Copié !",
|
||||
"common.copy": "Copier",
|
||||
"common.created": "Créé",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Supprimer",
|
||||
"common.description": "Description",
|
||||
"common.details": "Détails",
|
||||
"common.disabled": "Désactivé",
|
||||
"common.download": "Télécharger",
|
||||
"common.edit": "Modifier",
|
||||
"common.enabled": "Activé",
|
||||
"common.error": "Erreur",
|
||||
"common.failed": "Échoué",
|
||||
"common.filter": "Filtrer",
|
||||
"common.inactive": "Inactif",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Chargement...",
|
||||
"common.name": "Nom",
|
||||
"common.next": "Suivant",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "Non",
|
||||
"common.none": "Aucun",
|
||||
"common.page": "Page",
|
||||
"common.pending": "En attente",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "En cours de traitement",
|
||||
"common.refresh": "Actualiser",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.retry": "Réessayer",
|
||||
"common.save": "Enregistrer",
|
||||
"common.search": "Rechercher",
|
||||
"common.select": "Sélectionner",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Taille",
|
||||
"common.status": "Statut",
|
||||
"common.success": "Succès",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Mis à jour",
|
||||
"common.upload": "Téléverser",
|
||||
"common.view": "Voir",
|
||||
"common.warning": "Avertissement",
|
||||
"common.yes": "Oui",
|
||||
"cookie.accept": "Compris",
|
||||
"cookie.notice": "DocuElevate utilise uniquement des cookies de session essentiels nécessaires à l'authentification et au fonctionnement du service. Aucun cookie de suivi ou d'analyse n'est utilisé.",
|
||||
"cookie.notice_label": "Avis relatif aux cookies",
|
||||
"cookie.policy_link": "Politique de cookies",
|
||||
"cookie.privacy_link": "Avis de confidentialité",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Intégrations actives",
|
||||
"dashboard.files_this_month": "Fichiers ce mois-ci",
|
||||
"dashboard.files_today": "Fichiers aujourd'hui",
|
||||
"dashboard.ocr_processed": "OCR traités",
|
||||
"dashboard.quick_actions": "Actions rapides",
|
||||
"dashboard.recent_activity": "Activité récente",
|
||||
"dashboard.storage_targets": "Destinations de stockage",
|
||||
"dashboard.title": "Tableau de bord",
|
||||
"dashboard.total_files": "Total des fichiers",
|
||||
"dashboard.welcome": "Bienvenue sur DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Interdit",
|
||||
"error.forbidden_message": "Vous n'avez pas la permission d'accéder à cette page.",
|
||||
"error.not_found": "Page non trouvée",
|
||||
"error.not_found_message": "La page que vous recherchez n'existe pas.",
|
||||
"error.server_error": "Erreur interne du serveur",
|
||||
"error.server_error_message": "Quelque chose s'est mal passé. Veuillez réessayer plus tard.",
|
||||
"error.unauthorized": "Non autorisé",
|
||||
"error.unauthorized_message": "Vous devez vous connecter pour accéder à cette page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Titre du document",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "Taille du fichier",
|
||||
"files.filename": "Nom du fichier",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "Aucun fichier trouvé",
|
||||
"files.ocr_status": "Statut OCR",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Étiquettes",
|
||||
"files.title": "Fichiers",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Téléversé",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Mentions légales",
|
||||
"footer.license": "Licence",
|
||||
"footer.navigation": "Navigation du pied de page",
|
||||
"footer.privacy": "Confidentialité",
|
||||
"footer.terms": "Conditions",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Questions fréquentes",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Premiers pas",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Centre d'aide",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configurer",
|
||||
"integrations.connect": "Connecter",
|
||||
"integrations.connected": "Connecté",
|
||||
"integrations.disconnect": "Déconnecter",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Non connecté",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Intégrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Langue changée en {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Langue",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "À propos",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Actions admin",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.api_docs": "Documentation API",
|
||||
"nav.backup_restore": "Sauvegarde et restauration",
|
||||
"nav.credentials": "Identifiants",
|
||||
"nav.dark_mode": "Mode sombre",
|
||||
"nav.dashboard": "Tableau de bord",
|
||||
"nav.developer_docs": "Documentation développeur",
|
||||
"nav.duplicates": "Doublons",
|
||||
"nav.file_manager": "Gestionnaire de fichiers",
|
||||
"nav.files": "Fichiers",
|
||||
"nav.help": "Aide",
|
||||
"nav.help_center": "Centre d'aide",
|
||||
"nav.integrations": "Intégrations",
|
||||
"nav.light_mode": "Mode clair",
|
||||
"nav.main_navigation": "Navigation principale",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Ouvrir le menu principal",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Concepteur de plans",
|
||||
"nav.pricing": "Tarifs",
|
||||
"nav.queue_monitor": "File d'attente",
|
||||
"nav.scheduled_jobs": "Tâches planifiées",
|
||||
"nav.search": "Recherche",
|
||||
"nav.settings": "Paramètres",
|
||||
"nav.similarity": "Similarité",
|
||||
"nav.skip_to_content": "Aller au contenu principal",
|
||||
"nav.status": "Statut",
|
||||
"nav.toggle_dark_mode": "Basculer le mode sombre",
|
||||
"nav.toggle_nav": "Basculer le menu de navigation",
|
||||
"nav.upload": "Téléverser",
|
||||
"nav.users": "Utilisateurs",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Tout marquer comme lu",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Marquer comme lu",
|
||||
"notifications.no_notifications": "Aucune notification",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} notifications non lues",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Créer un pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Modifier le pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Pipelines de traitement",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "Aucun résultat trouvé",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Rechercher par nom, contenu, étiquettes...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} résultats trouvés",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Rechercher des documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Êtes-vous sûr de vouloir réinitialiser ce paramètre ?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Échec de l'enregistrement du paramètre",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Paramètre enregistré avec succès",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Glissez-déposez vos fichiers ici ou cliquez pour parcourir",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Échec du téléversement",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Taille maximale du fichier : {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Sélectionner un fichier",
|
||||
"upload.success": "Fichier téléversé avec succès",
|
||||
"upload.title": "Téléverser un document",
|
||||
"upload.uploading": "Téléversement en cours...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Athraíodh an teanga go {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Gaeilge",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Jezik je promijenjen na {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Hrvatski",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "A nyelv megváltozott erre: {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Magyar",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Log In",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profile",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Actions",
|
||||
"common.active": "Active",
|
||||
"common.all": "All",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completed",
|
||||
"common.confirm": "Confirm",
|
||||
"common.copied": "Copied!",
|
||||
"common.copy": "Copy",
|
||||
"common.created": "Created",
|
||||
"common.date": "Date",
|
||||
"common.delete": "Delete",
|
||||
"common.description": "Description",
|
||||
"common.details": "Details",
|
||||
"common.disabled": "Disabled",
|
||||
"common.download": "Download",
|
||||
"common.edit": "Edit",
|
||||
"common.enabled": "Enabled",
|
||||
"common.error": "Error",
|
||||
"common.failed": "Failed",
|
||||
"common.filter": "Filter",
|
||||
"common.inactive": "Inactive",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Loading...",
|
||||
"common.name": "Name",
|
||||
"common.next": "Next",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "None",
|
||||
"common.page": "Page",
|
||||
"common.pending": "Pending",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "Processing",
|
||||
"common.refresh": "Refresh",
|
||||
"common.reset": "Reset",
|
||||
"common.retry": "Retry",
|
||||
"common.save": "Save",
|
||||
"common.search": "Search",
|
||||
"common.select": "Select",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Size",
|
||||
"common.status": "Status",
|
||||
"common.success": "Success",
|
||||
"common.type": "Type",
|
||||
"common.updated": "Updated",
|
||||
"common.upload": "Upload",
|
||||
"common.view": "View",
|
||||
"common.warning": "Warning",
|
||||
"common.yes": "Yes",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Document Title",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "File Size",
|
||||
"files.filename": "Filename",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "No files found",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tags",
|
||||
"files.title": "Files",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Uploaded",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.license": "License",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Terms",
|
||||
"footer.version": "Version {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentation",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Support",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Help Center",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configure",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Tungumálið var breytt í {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Íslenska",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.files": "Files",
|
||||
"nav.help": "Help",
|
||||
"nav.help_center": "Help Center",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.search": "Search",
|
||||
"nav.settings": "Settings",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.status": "Status",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.upload": "Upload",
|
||||
"nav.users": "Users",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "No results found",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} results found",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Search Documents",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Settings",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
{
|
||||
"about.creator_heading": "Meet the Creator",
|
||||
"about.creator_name": "Christian Krakau-Louis",
|
||||
"about.creator_pre": "DocuElevate is passionately developed by",
|
||||
"about.features_admin_api": "Powerful REST API for programmatic access",
|
||||
"about.features_admin_config": "Highly configurable via environment variables",
|
||||
"about.features_admin_docker": "Docker-ready for easy deployment and scaling",
|
||||
"about.features_admin_heading": "Administration",
|
||||
"about.features_admin_oauth2": "OAuth2 authentication support with Authentik",
|
||||
"about.features_automation_background": "Background processing with Redis and Celery",
|
||||
"about.features_automation_gmail": "Gmail and generic email account integration",
|
||||
"about.features_automation_heading": "Automation",
|
||||
"about.features_automation_imap": "IMAP inbox polling from multiple sources",
|
||||
"about.features_automation_ingestion": "Automated document ingestion from various inputs",
|
||||
"about.features_heading": "Key Features",
|
||||
"about.features_integration_cloud": "Cloud storage: Dropbox, Google Drive, OneDrive, Amazon S3",
|
||||
"about.features_integration_heading": "Integration & Storage",
|
||||
"about.features_integration_multi_dest": "Multi-destination support (store documents in multiple locations)",
|
||||
"about.features_integration_protocols": "Transfer protocols: FTP, SFTP, Email forwarding",
|
||||
"about.features_integration_selfhosted": "Self-hosted options: Nextcloud, Paperless NGX, WebDAV",
|
||||
"about.features_processing_classification": "Intelligent document classification and date extraction",
|
||||
"about.features_processing_heading": "Document Processing",
|
||||
"about.features_processing_metadata": "Automated metadata extraction using a pluggable AI provider",
|
||||
"about.features_processing_ocr": "OCR powered by Azure Document Intelligence",
|
||||
"about.features_processing_pdf": "PDF conversion for various file formats via Gotenberg",
|
||||
"about.features_processing_upload": "Simple and secure file uploads with drag & drop support",
|
||||
"about.github_logo_alt": "GitHub Logo",
|
||||
"about.heading": "About DocuElevate",
|
||||
"about.intro_post": " – your modern, intelligent solution for document processing! We’ve built DocuElevate to completely transform the way you handle your documents – from upload to extraction, from processing to storage.",
|
||||
"about.intro_pre": "Welcome to ",
|
||||
"about.involved_description": "Want to dive into the code, contribute ideas, or learn more about DocuElevate?",
|
||||
"about.involved_docs": "Read the Documentation",
|
||||
"about.involved_github": "View DocuElevate on GitHub",
|
||||
"about.involved_heading": "Get Involved",
|
||||
"about.involved_website": "Visit DocuElevate Website",
|
||||
"about.legal_description": "We care about your privacy and data security. Please review our:",
|
||||
"about.legal_heading": "Privacy & Legal",
|
||||
"about.legal_license": "License Information",
|
||||
"about.legal_privacy": "Privacy Notice",
|
||||
"about.page_title": "About DocuElevate",
|
||||
"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 you’re 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.",
|
||||
"api_tokens.col_created": "Created",
|
||||
"api_tokens.col_last_ip": "Last IP",
|
||||
"api_tokens.col_last_used": "Last Used",
|
||||
"api_tokens.col_name": "Name",
|
||||
"api_tokens.col_prefix": "Token Prefix",
|
||||
"api_tokens.copy_to_clipboard": "Copy token to clipboard",
|
||||
"api_tokens.copy_upload_example": "Copy upload example to clipboard",
|
||||
"api_tokens.copy_warning_1": "Copy this token now — it will",
|
||||
"api_tokens.copy_warning_2": "not be shown again",
|
||||
"api_tokens.create_heading": "Create New Token",
|
||||
"api_tokens.create_token": "Create Token",
|
||||
"api_tokens.creating": "Creating…",
|
||||
"api_tokens.heading": "API Tokens",
|
||||
"api_tokens.intro": "Create personal API tokens to interact with the DocuElevate API programmatically. Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload or retrieve documents.",
|
||||
"api_tokens.loading_tokens": "Loading tokens…",
|
||||
"api_tokens.never": "Never",
|
||||
"api_tokens.no_tokens_heading": "No API tokens yet",
|
||||
"api_tokens.no_tokens_help": "Create your first token above to get started.",
|
||||
"api_tokens.page_title": "API Tokens – DocuElevate",
|
||||
"api_tokens.revoke": "Revoke",
|
||||
"api_tokens.revoke_prefix": "Revoke token",
|
||||
"api_tokens.status_active": "Active",
|
||||
"api_tokens.status_revoked": "Revoked",
|
||||
"api_tokens.table_aria": "API Tokens",
|
||||
"api_tokens.token_created": "Token created successfully!",
|
||||
"api_tokens.token_name_label": "Token name",
|
||||
"api_tokens.token_name_placeholder": "e.g. CI Pipeline, Webhook Upload, My Script",
|
||||
"api_tokens.usage_heading": "Usage Example",
|
||||
"api_tokens.usage_intro_post": "header with any API request:",
|
||||
"api_tokens.usage_intro_pre": "Use your API token in the",
|
||||
"api_tokens.your_tokens": "Your Tokens",
|
||||
"app.name": "DocuElevate",
|
||||
"audit.col_ip": "IP",
|
||||
"audit.col_resource": "Resource",
|
||||
"audit.col_timestamp": "Timestamp",
|
||||
"audit.critical": "Critical",
|
||||
"audit.filter_action": "Action",
|
||||
"audit.filter_all_actions": "All actions",
|
||||
"audit.filter_all_users": "All users",
|
||||
"audit.filter_resource_placeholder": "e.g. document, user",
|
||||
"audit.filter_resource_type": "Resource Type",
|
||||
"audit.filter_severity": "Severity",
|
||||
"audit.filter_user": "User",
|
||||
"audit.filters_section_label": "Audit log filters",
|
||||
"audit.next": "Next",
|
||||
"audit.next_label": "Next page",
|
||||
"audit.no_events": "No audit events recorded yet.",
|
||||
"audit.no_events_hint": "Significant actions (logins, document operations, settings changes) will appear here.",
|
||||
"audit.page_title": "Audit Logs - DocuElevate",
|
||||
"audit.pagination_label": "Audit log pagination",
|
||||
"audit.prev": "Prev",
|
||||
"audit.prev_label": "Previous page",
|
||||
"audit.refresh_label": "Refresh audit logs",
|
||||
"audit.siem_disabled_title": "SIEM forwarding is disabled",
|
||||
"audit.siem_enabled_title": "Events are being forwarded to ",
|
||||
"audit.siem_off": "SIEM: Off",
|
||||
"audit.subtitle": "Comprehensive, append-only record of all significant actions.",
|
||||
"audit.table_label": "Audit log events",
|
||||
"audit.title": "Audit Logs",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.create_account": "Create account",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"auth.email_label": "Email",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.forgot_username": "Forgot username?",
|
||||
"auth.login": "Accedi",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.logo_alt": "DocuElevate Logo",
|
||||
"auth.logout": "Esci",
|
||||
"auth.my_account": "Il mio account",
|
||||
"auth.no_account": "Don't have an account?",
|
||||
"auth.or_continue_with": "Or continue with",
|
||||
"auth.password_label": "Password",
|
||||
"auth.profile": "Profilo",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.return_home": "Return to Home",
|
||||
"auth.sign_in": "Sign in",
|
||||
"auth.sign_in_sso": "Sign in with SSO",
|
||||
"auth.sign_in_with": "Sign in with",
|
||||
"auth.sign_in_with_username": "Sign in with username",
|
||||
"auth.signup": "Registrati",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.username_label": "Username",
|
||||
"auth.username_or_email": "Username or Email",
|
||||
"auth.verify_email_back_sign_in": "Back to sign in",
|
||||
"auth.verify_email_expiry": "The link expires in 24 hours. If you don’t see the email, check your spam folder.",
|
||||
"auth.verify_email_heading": "Check your inbox",
|
||||
"auth.verify_email_message": "We’ve sent you a verification email. Please click the link in the email to activate your account.",
|
||||
"auth.verify_email_page_title": "DocuElevate - Verify Your Email",
|
||||
"auth.verify_email_resend_aria_label": "Email address for resend",
|
||||
"auth.verify_email_resend_button": "Resend verification email",
|
||||
"auth.verify_email_resend_label": "Email address",
|
||||
"auth.verify_email_resend_placeholder": "Enter your email address",
|
||||
"auth.verify_email_resend_prompt": "Didn’t receive it?",
|
||||
"backup.archives_heading": "Backup Archives",
|
||||
"backup.backup_now": "Backup Now",
|
||||
"backup.clean_up": "Clean Up",
|
||||
"backup.cleanup_title": "Run retention cleanup now",
|
||||
"backup.col_created": "Created",
|
||||
"backup.col_filename": "Filename",
|
||||
"backup.col_size": "Size",
|
||||
"backup.col_storage": "Storage",
|
||||
"backup.col_type": "Type",
|
||||
"backup.config_auto_backup": "Auto-backup",
|
||||
"backup.config_remote_dest": "Remote destination",
|
||||
"backup.config_retention": "Retention",
|
||||
"backup.config_total_size": "Total local size",
|
||||
"backup.confirm_btn": "Confirm",
|
||||
"backup.confirm_title": "Confirm action",
|
||||
"backup.heading": "Backup Management",
|
||||
"backup.local_only": "Local only",
|
||||
"backup.no_backups": "No backups yet.",
|
||||
"backup.no_backups_hint": "Click Backup Now to create your first backup.",
|
||||
"backup.page_title": "Backup Management",
|
||||
"backup.records_label": "records",
|
||||
"backup.restore_btn": "Restore",
|
||||
"backup.restore_desc_mid": "backup archive to restore the database.",
|
||||
"backup.restore_desc_pre": "Upload a",
|
||||
"backup.restore_desc_warning": "This will overwrite all current data.",
|
||||
"backup.restore_file_label": "Backup archive (.db.gz)",
|
||||
"backup.restore_heading": "Restore from File",
|
||||
"backup.restoring": "Restoring…",
|
||||
"backup.retention_daily_detail": "– kept for 3 weeks",
|
||||
"backup.retention_heading": "Retention policy",
|
||||
"backup.retention_hourly_detail": "– kept for 4 days",
|
||||
"backup.retention_note": "Backups beyond these limits are automatically pruned after each new backup is created.",
|
||||
"backup.retention_weekly_detail": "– kept for ~3 months",
|
||||
"backup.snapshots": "snapshots",
|
||||
"backup.status_ok": "ok",
|
||||
"backup.storage_local": "local",
|
||||
"backup.subtitle": "Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).",
|
||||
"backup.table_aria": "Backup archives",
|
||||
"backup.trigger_daily": "Backup Now (Daily)",
|
||||
"backup.trigger_hourly": "Backup Now (Hourly)",
|
||||
"backup.trigger_weekly": "Backup Now (Weekly)",
|
||||
"backup.type_daily": "Daily",
|
||||
"backup.type_hourly": "Hourly",
|
||||
"backup.type_weekly": "Weekly",
|
||||
"billing.go_to_dashboard": "Go to dashboard",
|
||||
"billing.manage_subscription": "Manage subscription",
|
||||
"billing.success_heading": "You're all set!",
|
||||
"billing.success_message": "Your subscription has been activated. Thank you for choosing DocuElevate!",
|
||||
"billing.success_page_title": "DocuElevate - Subscription Activated",
|
||||
"common.actions": "Azioni",
|
||||
"common.active": "Attivo",
|
||||
"common.all": "Tutto",
|
||||
"common.back": "Indietro",
|
||||
"common.cancel": "Annulla",
|
||||
"common.close": "Chiudi",
|
||||
"common.col_pending": "Pending",
|
||||
"common.completed": "Completato",
|
||||
"common.confirm": "Conferma",
|
||||
"common.copied": "Copiato!",
|
||||
"common.copy": "Copia",
|
||||
"common.created": "Creato",
|
||||
"common.date": "Data",
|
||||
"common.delete": "Elimina",
|
||||
"common.description": "Descrizione",
|
||||
"common.details": "Dettagli",
|
||||
"common.disabled": "Disabilitato",
|
||||
"common.download": "Scarica",
|
||||
"common.edit": "Modifica",
|
||||
"common.enabled": "Abilitato",
|
||||
"common.error": "Errore",
|
||||
"common.failed": "Fallito",
|
||||
"common.filter": "Filtra",
|
||||
"common.inactive": "Inattivo",
|
||||
"common.info": "Info",
|
||||
"common.loading": "Caricamento...",
|
||||
"common.name": "Nome",
|
||||
"common.next": "Avanti",
|
||||
"common.next_page": "Next page",
|
||||
"common.no": "No",
|
||||
"common.none": "Nessuno",
|
||||
"common.page": "Page",
|
||||
"common.pending": "In attesa",
|
||||
"common.prev_page": "Previous page",
|
||||
"common.processing": "In elaborazione",
|
||||
"common.refresh": "Aggiorna",
|
||||
"common.reset": "Reimposta",
|
||||
"common.retry": "Riprova",
|
||||
"common.save": "Salva",
|
||||
"common.search": "Cerca",
|
||||
"common.select": "Seleziona",
|
||||
"common.select_placeholder": "— select —",
|
||||
"common.size": "Dimensione",
|
||||
"common.status": "Stato",
|
||||
"common.success": "Successo",
|
||||
"common.type": "Tipo",
|
||||
"common.updated": "Aggiornato",
|
||||
"common.upload": "Carica",
|
||||
"common.view": "Visualizza",
|
||||
"common.warning": "Avviso",
|
||||
"common.yes": "Sì",
|
||||
"cookie.accept": "Ho capito",
|
||||
"cookie.notice": "DocuElevate utilizza solo cookie di sessione essenziali necessari per l'autenticazione e il funzionamento del servizio. Non vengono utilizzati cookie di tracciamento o analisi.",
|
||||
"cookie.notice_label": "Avviso sui cookie",
|
||||
"cookie.policy_link": "Politica sui cookie",
|
||||
"cookie.privacy_link": "Informativa sulla privacy",
|
||||
"credentials.col_action": "Action",
|
||||
"credentials.col_credential": "Credential",
|
||||
"credentials.col_source": "Source",
|
||||
"credentials.configured": "Configured",
|
||||
"credentials.edit_in_settings": "Edit in Settings",
|
||||
"credentials.legend_db": "Value stored in the database (encrypted at rest; overrides environment variable)",
|
||||
"credentials.legend_env_after": " file",
|
||||
"credentials.legend_env_before": "Value from environment variable or ",
|
||||
"credentials.legend_missing": "Credential not set — integration will not work",
|
||||
"credentials.legend_restart": "Restart required when this credential is rotated",
|
||||
"credentials.legend_title": "Legend",
|
||||
"credentials.manage_settings": "Manage Settings",
|
||||
"credentials.not_configured": "Not Configured",
|
||||
"credentials.page_title": "Credential Audit - DocuElevate",
|
||||
"credentials.raw_json": "Raw JSON (API)",
|
||||
"credentials.restart_title": "Restart required after rotating this credential",
|
||||
"credentials.source_db_title": "Stored in database (encrypted)",
|
||||
"credentials.source_env_title": "From environment variable",
|
||||
"credentials.status_missing": "Missing",
|
||||
"credentials.subtitle": "Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only whether each credential is configured and where it comes from.",
|
||||
"credentials.table_for": "Credentials for",
|
||||
"credentials.title": "Credential Audit",
|
||||
"credentials.total_credentials": "Total Credentials",
|
||||
"dashboard.active_integrations": "Integrazioni attive",
|
||||
"dashboard.files_this_month": "File questo mese",
|
||||
"dashboard.files_today": "File oggi",
|
||||
"dashboard.ocr_processed": "OCR elaborati",
|
||||
"dashboard.quick_actions": "Azioni rapide",
|
||||
"dashboard.recent_activity": "Attività recente",
|
||||
"dashboard.storage_targets": "Destinazioni di archiviazione",
|
||||
"dashboard.title": "Cruscotto",
|
||||
"dashboard.total_files": "File totali",
|
||||
"dashboard.welcome": "Benvenuto su DocuElevate",
|
||||
"duplicates.file_id_label": "File ID",
|
||||
"duplicates.file_id_placeholder": "e.g. 42",
|
||||
"duplicates.find_btn": "Find",
|
||||
"duplicates.found_files": "duplicate file(s) total.",
|
||||
"duplicates.found_groups": "duplicate group(s) with",
|
||||
"duplicates.found_prefix": "Found",
|
||||
"duplicates.group_aria": "Duplicate group",
|
||||
"duplicates.heading": "Duplicate Documents",
|
||||
"duplicates.how_it_works_heading": "How near-duplicate detection works",
|
||||
"duplicates.max_results_label": "Max results",
|
||||
"duplicates.near_dup_desc": "Select a document to check whether any other files contain the same (or very similar) content — even if they were scanned at different times and have different SHA-256 hashes.",
|
||||
"duplicates.near_dup_heading": "Find Near-Duplicates for a Document",
|
||||
"duplicates.no_exact_heading": "No exact duplicates found",
|
||||
"duplicates.page_title": "Duplicate Documents - DocuElevate",
|
||||
"duplicates.pagination_aria": "Pagination",
|
||||
"duplicates.role_duplicate": "Duplicate",
|
||||
"duplicates.role_original": "Original",
|
||||
"duplicates.tab_exact": "Exact Duplicates",
|
||||
"duplicates.tab_near": "Near-Duplicate Finder",
|
||||
"duplicates.tabs_aria": "Duplicate detection tabs",
|
||||
"duplicates.threshold_label": "Similarity threshold",
|
||||
"duplicates.view_dup_aria": "View duplicate file",
|
||||
"duplicates.view_original_aria": "View original file",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_home": "Return Home",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_title": "404 - Not Found",
|
||||
"error.500_code": "500",
|
||||
"error.500_debug_info": "Show Debug Info",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_home": "Go Home",
|
||||
"error.500_img_alt": "Illustration of a server error",
|
||||
"error.500_message1": "Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?",
|
||||
"error.500_message2": "We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.",
|
||||
"error.500_title": "Server Error - DocuElevate",
|
||||
"error.forbidden": "Vietato",
|
||||
"error.forbidden_message": "Non hai il permesso di accedere a questa pagina.",
|
||||
"error.not_found": "Pagina non trovata",
|
||||
"error.not_found_message": "La pagina che stai cercando non esiste.",
|
||||
"error.server_error": "Errore interno del server",
|
||||
"error.server_error_message": "Qualcosa è andato storto. Riprova più tardi.",
|
||||
"error.unauthorized": "Non autorizzato",
|
||||
"error.unauthorized_message": "Devi accedere per visualizzare questa pagina.",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.action_details": "View details",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.document_title": "Titolo del documento",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.error_hint": "This might be due to a configuration or import issue. Please check the server logs.",
|
||||
"files.file_size": "Dimensione del file",
|
||||
"files.filename": "Nome del file",
|
||||
"files.files_selected": "files selected",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_from_aria": "Filter from date",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_date_to_aria": "Filter to date",
|
||||
"files.filter_form_aria": "Filter files",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_quality_aria": "Filter by OCR quality score",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_search_label": "Search Filename",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_tags_aria": "Filter by tags (comma-separated)",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.no_files": "Nessun file trovato",
|
||||
"files.ocr_status": "Stato OCR",
|
||||
"files.page_title": "File Records",
|
||||
"files.pagination_files": "files",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_last": "Last",
|
||||
"files.pagination_nav_aria": "File list pagination",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_of": "of",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_showing": "Showing",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.queue_banner_items": "item(s) are currently queued or being processed. Files will appear here once processing completes.",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_save_aria": "Save current filters as a saved search",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.status_duplicate": "Duplicate",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_aria": "File records",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_empty": "No files found",
|
||||
"files.table_id": "ID",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.tags": "Tag",
|
||||
"files.title": "File",
|
||||
"files.upload_modal_aria": "Upload progress",
|
||||
"files.upload_modal_close": "Close upload progress",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.uploaded": "Caricato",
|
||||
"footer.attributions": "Attribuzioni",
|
||||
"footer.cookies": "Cookie",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.imprint": "Note legali",
|
||||
"footer.license": "Licenza",
|
||||
"footer.navigation": "Navigazione a piè di pagina",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.terms": "Termini",
|
||||
"footer.version": "Versione {version}",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.documentation": "Documentazione",
|
||||
"help.faq": "Domande frequenti",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.getting_started": "Per iniziare",
|
||||
"help.heading": "Help Center",
|
||||
"help.page_title": "Help Center",
|
||||
"help.privacy_notice": "Privacy Notice",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.support": "Supporto",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_ticket_button": "Submit a Ticket",
|
||||
"help.support_ticket_heading": "Open a Support Ticket",
|
||||
"help.title": "Centro assistenza",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.button_browse_files": "Browse Files",
|
||||
"index.button_upload": "Upload",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.dashboard_subtitle_teams": "Intelligent document processing & management — built for teams",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_system_status": "System Status",
|
||||
"index.quick_system_status_desc": "Check integration health",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_view_files": "View All Files",
|
||||
"index.quick_view_files_desc": "Browse processed documents",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.stat_active_integrations": "Active Integrations",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_storage_targets": "Storage Targets",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"integrations.configure": "Configura",
|
||||
"integrations.connect": "Connetti",
|
||||
"integrations.connected": "Connesso",
|
||||
"integrations.disconnect": "Disconnetti",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.not_connected": "Non connesso",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.title": "Integrazioni",
|
||||
"integrations.username_label": "Username",
|
||||
"language.bg": "Български",
|
||||
"language.ca": "Català",
|
||||
"language.changed": "Lingua cambiata in {language}",
|
||||
"language.cs": "Čeština",
|
||||
"language.da": "Dansk",
|
||||
"language.de": "Deutsch",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.en": "English",
|
||||
"language.es": "Español",
|
||||
"language.et": "Eesti",
|
||||
"language.fi": "Suomi",
|
||||
"language.fr": "Français",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.hu": "Magyar",
|
||||
"language.is": "Íslenska",
|
||||
"language.it": "Italiano",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.selector": "Lingua",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.sv": "Svenska",
|
||||
"language.tr": "Türkçe",
|
||||
"language.uk": "Українська",
|
||||
"language.zh": "中文",
|
||||
"nav.about": "Informazioni",
|
||||
"nav.admin": "Admin",
|
||||
"nav.admin_actions": "Azioni admin",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.api_docs": "Documentazione API",
|
||||
"nav.backup_restore": "Backup e ripristino",
|
||||
"nav.credentials": "Credenziali",
|
||||
"nav.dark_mode": "Modalità scura",
|
||||
"nav.dashboard": "Cruscotto",
|
||||
"nav.developer_docs": "Documentazione sviluppatore",
|
||||
"nav.duplicates": "Duplicati",
|
||||
"nav.file_manager": "Gestore file",
|
||||
"nav.files": "File",
|
||||
"nav.help": "Aiuto",
|
||||
"nav.help_center": "Centro assistenza",
|
||||
"nav.integrations": "Integrazioni",
|
||||
"nav.light_mode": "Modalità chiara",
|
||||
"nav.main_navigation": "Navigazione principale",
|
||||
"nav.notifications": "Notifiche",
|
||||
"nav.open_main_menu": "Apri menu principale",
|
||||
"nav.pipelines": "Pipeline",
|
||||
"nav.plan_designer": "Designer dei piani",
|
||||
"nav.pricing": "Prezzi",
|
||||
"nav.queue_monitor": "Monitor coda",
|
||||
"nav.scheduled_jobs": "Attività pianificate",
|
||||
"nav.search": "Cerca",
|
||||
"nav.settings": "Impostazioni",
|
||||
"nav.similarity": "Similarità",
|
||||
"nav.skip_to_content": "Vai al contenuto principale",
|
||||
"nav.status": "Stato",
|
||||
"nav.toggle_dark_mode": "Attiva/disattiva modalità scura",
|
||||
"nav.toggle_nav": "Attiva/disattiva menu di navigazione",
|
||||
"nav.upload": "Carica",
|
||||
"nav.users": "Utenti",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.mark_all_read": "Segna tutto come letto",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"notifications.mark_read": "Segna come letto",
|
||||
"notifications.no_notifications": "Nessuna notifica",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.title": "Notifiche",
|
||||
"notifications.unread_count": "{count} notifiche non lette",
|
||||
"pipelines.active_label": "Active",
|
||||
"pipelines.add_step_btn": "Add Step",
|
||||
"pipelines.create": "Crea pipeline",
|
||||
"pipelines.custom_label_label": "Custom Label",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.description_placeholder": "Optional description",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.edit": "Modifica pipeline",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.empty_state_hint": "Create your first pipeline to define custom document processing workflows.",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.force_cloud_ocr_label": "Force cloud OCR (skip local text extraction)",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.loading": "Loading pipelines…",
|
||||
"pipelines.name_placeholder": "My pipeline",
|
||||
"pipelines.new_pipeline_btn": "New Pipeline",
|
||||
"pipelines.no_steps": "No steps defined. Add a step to start building your pipeline.",
|
||||
"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.",
|
||||
"pipelines.ocr_language_label": "OCR Language",
|
||||
"pipelines.optional_suffix": "(optional)",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.step_label_placeholder": "Override the default step name",
|
||||
"pipelines.step_type_label": "Step Type",
|
||||
"pipelines.subtitle_intro": "Define and manage custom document processing workflows.",
|
||||
"pipelines.subtitle_system_post": "badge and are visible to all users.",
|
||||
"pipelines.subtitle_system_pre": "System pipelines (created by admins) are shown with a",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.system_pipeline_label": "System pipeline (visible to all users)",
|
||||
"pipelines.title": "Pipeline di elaborazione",
|
||||
"queue.active_tasks": "Active Tasks",
|
||||
"queue.auto_refresh_1": "Auto-refreshes every",
|
||||
"queue.auto_refresh_2": "seconds",
|
||||
"queue.col_arguments": "Arguments",
|
||||
"queue.col_current_step": "Current Step",
|
||||
"queue.col_file": "File",
|
||||
"queue.col_files": "Files",
|
||||
"queue.col_queue": "Queue",
|
||||
"queue.col_state": "State",
|
||||
"queue.col_task": "Task",
|
||||
"queue.col_task_id": "Task ID",
|
||||
"queue.files_processing": "Files Processing",
|
||||
"queue.heading": "Queue Monitor",
|
||||
"queue.last_updated": "Last updated:",
|
||||
"queue.loading_stats": "Loading queue statistics…",
|
||||
"queue.no_active_tasks": "No active tasks",
|
||||
"queue.no_data": "No data",
|
||||
"queue.no_files_processing": "No files currently processing",
|
||||
"queue.page_title": "Queue Monitor",
|
||||
"queue.processing_pipeline": "Processing Pipeline",
|
||||
"queue.queued_tasks": "Queued Tasks",
|
||||
"queue.recently_processing": "Recently Processing Files",
|
||||
"queue.redis_queues": "Redis Queues",
|
||||
"queue.subtitle": "Real-time view of the document processing pipeline and Celery task queues.",
|
||||
"queue.workers_online": "Workers Online",
|
||||
"search.button": "Search",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"search.filter_aria_clear": "Clear all filters",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_tags": "Tags",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_aria_label": "Search documents",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.no_results": "Nessun risultato trovato",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.placeholder": "Cerca per nome, contenuto, tag...",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.results_count": "{count} risultati trovati",
|
||||
"search.saved_aria_save": "Save current search",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.title": "Cerca documenti",
|
||||
"settings.audit_log_btn": "Audit Log",
|
||||
"settings.autocomplete_hint": "Type to search known values, or enter any custom value.",
|
||||
"settings.autocomplete_no_matches": "No matches — you can still type a custom value",
|
||||
"settings.autocomplete_placeholder": "Type to search or enter a value…",
|
||||
"settings.boolean_enable_prefix": "Enable",
|
||||
"settings.categories_aria": "Settings categories",
|
||||
"settings.categories_heading": "Categories",
|
||||
"settings.db_wizard_btn": "DB Wizard",
|
||||
"settings.effective_value_label": "Effective value:",
|
||||
"settings.encrypted_suffix": "= encrypted at rest",
|
||||
"settings.encrypted_title": "Value is encrypted at rest in database",
|
||||
"settings.export_btn": "Export",
|
||||
"settings.export_db_only": "DB settings only",
|
||||
"settings.export_full_config": "Full effective config",
|
||||
"settings.jump_to_category": "Jump to category…",
|
||||
"settings.manage_config_prefix": "Manage configuration. Priority:",
|
||||
"settings.model_picker_hint": "Pick from the list or type any model name supported by your provider.",
|
||||
"settings.model_picker_placeholder": "Select a common model or type a custom name…",
|
||||
"settings.no_results_clear": "clear the search",
|
||||
"settings.no_results_heading": "No settings found",
|
||||
"settings.no_results_hint": "Try a different search term or",
|
||||
"settings.page_heading": "Application Settings",
|
||||
"settings.required_label": "(required)",
|
||||
"settings.reset_confirm": "Sei sicuro di voler reimpostare questa impostazione?",
|
||||
"settings.restart_required_suffix": "= restart required",
|
||||
"settings.revert_btn": "Remove from DB",
|
||||
"settings.revert_title": "Remove DB override and revert to environment variable or default",
|
||||
"settings.reverting": "Reverting…",
|
||||
"settings.save_all_btn": "Save All Changes",
|
||||
"settings.save_error": "Salvataggio impostazione fallito",
|
||||
"settings.save_setting_title": "Save this setting",
|
||||
"settings.save_success": "Impostazione salvata con successo",
|
||||
"settings.saving_state": "Saving…",
|
||||
"settings.search_aria_label": "Search settings",
|
||||
"settings.search_clear_aria": "Clear search",
|
||||
"settings.search_placeholder": "Search settings by name, key, or description…",
|
||||
"settings.settings_count_suffix": "settings",
|
||||
"settings.source_db_badge": "DB",
|
||||
"settings.source_default_badge": "DEFAULT",
|
||||
"settings.source_env_badge": "ENV",
|
||||
"settings.title": "Impostazioni",
|
||||
"settings.toggle_visibility_aria": "Toggle password visibility",
|
||||
"settings.toggle_visibility_title": "Show/hide 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.wizard_btn": "Wizard",
|
||||
"shared.col_expiry": "Expiry",
|
||||
"shared.col_file_label": "File / Label",
|
||||
"shared.col_link": "Link",
|
||||
"shared.col_views": "Views",
|
||||
"shared.copy_link_aria": "Copy link to clipboard",
|
||||
"shared.copy_link_title": "Copy link",
|
||||
"shared.create_btn": "Create Link",
|
||||
"shared.create_heading": "Create New Shared Link",
|
||||
"shared.creating": "Creating…",
|
||||
"shared.expiry_12h": "12 hours",
|
||||
"shared.expiry_14d": "14 days",
|
||||
"shared.expiry_1h": "1 hour",
|
||||
"shared.expiry_24h": "24 hours (1 day)",
|
||||
"shared.expiry_30d": "30 days",
|
||||
"shared.expiry_3d": "3 days",
|
||||
"shared.expiry_6h": "6 hours",
|
||||
"shared.expiry_7d": "7 days",
|
||||
"shared.expiry_label": "Expiry",
|
||||
"shared.expiry_never": "Never",
|
||||
"shared.file_id_help_post": "page or in the document detail URL.",
|
||||
"shared.file_id_help_pre": "Find the file ID on the",
|
||||
"shared.file_id_label": "File ID",
|
||||
"shared.file_id_placeholder": "e.g. 42",
|
||||
"shared.files_link": "Files",
|
||||
"shared.heading": "Shared Links",
|
||||
"shared.label_label": "Label",
|
||||
"shared.label_placeholder": "e.g. Shared with Bob",
|
||||
"shared.link_created": "Shared link created!",
|
||||
"shared.link_created_help": "Copy and send this link to the recipient.",
|
||||
"shared.links_count_aria": "number of links",
|
||||
"shared.max_downloads_label": "Max downloads",
|
||||
"shared.no_links": "No shared links yet. Create one above to get started.",
|
||||
"shared.open_in_new_tab": "Open in new tab",
|
||||
"shared.open_link_aria": "Open shared link",
|
||||
"shared.optional": "(optional)",
|
||||
"shared.page_title": "Shared Links – DocuElevate",
|
||||
"shared.password_label": "Password",
|
||||
"shared.password_placeholder": "Leave blank for no password",
|
||||
"shared.password_protected": "Password protected",
|
||||
"shared.refresh_aria": "Refresh shared links list",
|
||||
"shared.revoke_aria_prefix": "Revoke shared link for",
|
||||
"shared.revoke_btn": "Revoke",
|
||||
"shared.status_active": "Active",
|
||||
"shared.status_expired": "Expired",
|
||||
"shared.status_limit_reached": "Limit reached",
|
||||
"shared.status_revoked": "Revoked",
|
||||
"shared.subtitle": "Share documents with anyone via a time-limited or view-limited link. Recipients do not need a DocuElevate account. Links can be password-protected and revoked at any time.",
|
||||
"shared.table_aria": "Shared links",
|
||||
"shared.unlimited_placeholder": "Unlimited",
|
||||
"shared.your_links": "Your Shared Links",
|
||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||
"similarity.find_pairs_btn": "Find Pairs",
|
||||
"similarity.heading": "Document Similarity",
|
||||
"similarity.load_error": "Failed to load pairs.",
|
||||
"similarity.min_similarity_label": "Min. similarity",
|
||||
"similarity.no_pairs_detail": "No document pairs exceed the similarity threshold.",
|
||||
"similarity.no_pairs_heading": "No similar pairs found",
|
||||
"similarity.page_title": "Document Similarity - DocuElevate",
|
||||
"similarity.pagination_aria": "Similarity pairs pagination",
|
||||
"similarity.per_page_label": "Per page",
|
||||
"similarity.scanning": "Scanning for similar document pairs…",
|
||||
"similarity.stat_embedding_model": "Embedding Model",
|
||||
"similarity.stat_missing_embedding": "Missing Embedding",
|
||||
"similarity.stat_total_files": "Total Files",
|
||||
"similarity.stat_with_embedding": "With Embedding",
|
||||
"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.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.last_check": "Last Check",
|
||||
"status.page_title": "System Status",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.button_processing": "Processing...",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.drag_drop": "Trascina i file qui o fai clic per sfogliare",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.drop_zone_aria": "File upload area. Drag and drop files here, or press Enter to browse files.",
|
||||
"upload.error": "Caricamento fallito",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.max_size": "Dimensione massima del file: {size}",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.select_file": "Seleziona file",
|
||||
"upload.success": "File caricato con successo",
|
||||
"upload.title": "Carica documento",
|
||||
"upload.uploading": "Caricamento in corso...",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user