fix(merge): resolve conflicts in app/api/__init__.py and migrations/env.py against main
Merge main into branch to pull in translation files, audit log templates, CI workflow updates, and other changes from PRs #595, #597, #598. Keep mobile_router and MobileDevice additions from this branch.
This commit is contained in:
@@ -96,6 +96,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
|
||||
|
||||
@@ -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-11T11:44:20Z
|
||||
|
||||
@@ -10,6 +10,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
<!-- version list -->
|
||||
|
||||
## 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.116.0
|
||||
Build Date: 2026-03-11T11:44:20Z
|
||||
Git Commit: 9f7d6c85488081f546e7c392bc46b2827bb1caa6
|
||||
Git Short SHA: 9f7d6c8
|
||||
Git Branch: main
|
||||
Commit Date: 2026-03-10T00:00:39+01:00
|
||||
Build Timestamp: 2026-03-09T23:00:57Z
|
||||
Commit Date: 2026-03-11T12:43:59+01:00
|
||||
Build Timestamp: 2026-03-11T11:44:20Z
|
||||
==============================
|
||||
|
||||
@@ -398,6 +398,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.
|
||||
|
||||
@@ -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,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Audit Logs - DocuElevate{% 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 Logs
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Comprehensive, append-only record of all significant actions.
|
||||
</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="Events are being forwarded to {{ 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="SIEM forwarding is disabled">
|
||||
<i class="fas fa-tower-broadcast mr-1" aria-hidden="true"></i>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="Refresh audit logs">
|
||||
<i class="fas fa-arrows-rotate mr-1" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-4 mb-6" aria-label="Audit log filters">
|
||||
<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">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="">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">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="">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">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="">All</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-resource" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Resource Type</label>
|
||||
<input id="filter-resource" type="text" x-model.debounce.300ms="filters.resource_type" @input="fetchLogs()"
|
||||
placeholder="e.g. document, user"
|
||||
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>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 log events">
|
||||
<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">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">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">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">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">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">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">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">No audit events recorded yet.</p>
|
||||
<p class="text-sm mt-1">Significant actions (logins, document operations, settings changes) will appear here.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav class="flex items-center justify-between mt-4" aria-label="Audit log pagination" 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="Previous page">
|
||||
<i class="fas fa-chevron-left mr-1" aria-hidden="true"></i>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="Next page">
|
||||
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 %}
|
||||
+146
-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/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="/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,84 @@
|
||||
<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/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 +410,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 +421,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 +445,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 +467,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 +501,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() {
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"nav.dashboard": "Übersicht",
|
||||
"nav.upload": "Hochladen",
|
||||
"nav.files": "Dateien",
|
||||
"nav.search": "Suche",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.help": "Hilfe",
|
||||
"nav.settings": "Einstellungen",
|
||||
"nav.login": "Anmelden",
|
||||
"nav.logout": "Abmelden",
|
||||
"nav.signup": "Registrieren",
|
||||
"nav.profile": "Profil",
|
||||
"nav.admin": "Administration",
|
||||
"nav.admin.users": "Benutzer",
|
||||
"nav.admin.plans": "Tarife",
|
||||
"nav.admin.scheduled_jobs": "Geplante Aufgaben",
|
||||
"nav.admin.backups": "Sicherungen",
|
||||
"nav.admin.audit_logs": "Prüfprotokolle",
|
||||
"nav.queue": "Warteschlange",
|
||||
"nav.integrations": "Integrationen",
|
||||
"nav.status": "Systemstatus",
|
||||
"nav.notifications": "Benachrichtigungen",
|
||||
"nav.shared_links": "Geteilte Links",
|
||||
"nav.duplicates": "Duplikate",
|
||||
"nav.api_tokens": "API-Token",
|
||||
"nav.subscription": "Abonnement",
|
||||
"nav.imap": "E-Mail-Import",
|
||||
"nav.version": "Versionsinformationen",
|
||||
"footer.copyright": "© {year} DocuElevate",
|
||||
"footer.about": "Über uns",
|
||||
"footer.privacy": "Datenschutz",
|
||||
"footer.terms": "Nutzungsbedingungen",
|
||||
"footer.cookies": "Cookie-Richtlinie",
|
||||
"footer.imprint": "Impressum",
|
||||
"footer.attribution": "Namensnennung",
|
||||
"footer.license": "Lizenz",
|
||||
"footer.version": "Version",
|
||||
"common.loading": "Laden...",
|
||||
"common.save": "Speichern",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.delete": "Löschen",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.create": "Erstellen",
|
||||
"common.confirm": "Bestätigen",
|
||||
"common.close": "Schließen",
|
||||
"common.back": "Zurück",
|
||||
"common.next": "Weiter",
|
||||
"common.previous": "Zurück",
|
||||
"common.search": "Suche",
|
||||
"common.filter": "Filtern",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.submit": "Absenden",
|
||||
"common.download": "Herunterladen",
|
||||
"common.upload": "Hochladen",
|
||||
"common.actions": "Aktionen",
|
||||
"common.status": "Status",
|
||||
"common.name": "Name",
|
||||
"common.description": "Beschreibung",
|
||||
"common.type": "Typ",
|
||||
"common.date": "Datum",
|
||||
"common.size": "Größe",
|
||||
"common.enabled": "Aktiviert",
|
||||
"common.disabled": "Deaktiviert",
|
||||
"common.yes": "Ja",
|
||||
"common.no": "Nein",
|
||||
"common.none": "Keine",
|
||||
"common.error": "Fehler",
|
||||
"common.success": "Erfolg",
|
||||
"common.warning": "Warnung",
|
||||
"common.info": "Info",
|
||||
"common.tags": "Tags",
|
||||
"common.pending": "Ausstehend",
|
||||
"common.processing": "Verarbeitung",
|
||||
"common.completed": "Abgeschlossen",
|
||||
"common.failed": "Fehlgeschlagen",
|
||||
"common.duplicate": "Duplikat",
|
||||
"common.active": "Aktiv",
|
||||
"cookie.message": "Diese Website verwendet Cookies, um Ihr Erlebnis zu verbessern.",
|
||||
"cookie.accept": "Akzeptieren",
|
||||
"cookie.learn_more": "Mehr erfahren",
|
||||
"language.selector_label": "Sprache wählen",
|
||||
"language.change_success": "Sprache geändert zu {language}",
|
||||
"upload.page_title": "Dateien hochladen",
|
||||
"upload.section_device": "Vom Gerät hochladen",
|
||||
"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.browse_button": "Dateien durchsuchen",
|
||||
"upload.file_types": "Erlaubte Typen: PDF, Office-Dokumente (Word, Excel, PowerPoint usw.), Bilder",
|
||||
"upload.file_size_hint": "Maximale Größe: 500 MB pro Datei",
|
||||
"upload.camera_button": "Foto aufnehmen / Dokument scannen",
|
||||
"upload.section_url": "Von URL hochladen",
|
||||
"upload.url_label": "Datei-URL",
|
||||
"upload.url_placeholder": "https://beispiel.de/dokument.pdf",
|
||||
"upload.url_description": "Geben Sie einen direkten Link zu einer Datei ein (PDF, Office-Dokumente oder Bilder)",
|
||||
"upload.filename_label": "Dateiname (optional)",
|
||||
"upload.filename_placeholder": "mein-dokument.pdf",
|
||||
"upload.filename_description": "Leer lassen, um den Dateinamen aus der URL zu verwenden",
|
||||
"upload.download_button": "Herunterladen und verarbeiten",
|
||||
"upload.error_url_required": "Bitte geben Sie eine URL ein",
|
||||
"upload.error_invalid_url": "Ungültiges URL-Format",
|
||||
"upload.downloading": "Datei wird von URL heruntergeladen...",
|
||||
"upload.button_processing": "Verarbeitung...",
|
||||
"files.page_title": "Dateiübersicht",
|
||||
"files.drop_overlay_title": "Dateien oder Ordner zum Hochladen hier ablegen",
|
||||
"files.drop_overlay_hint": "Unterstützt PDF, Office-Dokumente, Bilder, HTML, Markdown und mehr",
|
||||
"files.upload_modal_header": "Dateien hochladen",
|
||||
"files.queue_banner_link": "Warteschlange ansehen",
|
||||
"files.filter_search_placeholder": "Dateinamen eingeben...",
|
||||
"files.filter_mime_type": "MIME-Typ",
|
||||
"files.filter_all_types": "Alle Typen",
|
||||
"files.filter_all_statuses": "Alle Status",
|
||||
"files.filter_date_from": "Datum von",
|
||||
"files.filter_date_to": "Datum bis",
|
||||
"files.filter_storage_provider": "Speicheranbieter",
|
||||
"files.filter_all_providers": "Alle Anbieter",
|
||||
"files.filter_tags_placeholder": "z.B. Rechnung,Amazon",
|
||||
"files.filter_ocr_quality": "OCR-Qualität",
|
||||
"files.filter_ocr_all": "Alle Dateien",
|
||||
"files.filter_ocr_poor": "Schlechte Qualität",
|
||||
"files.filter_ocr_good": "Gute Qualität",
|
||||
"files.filter_ocr_unchecked": "Noch nicht bewertet",
|
||||
"files.filter_apply": "Filter anwenden",
|
||||
"files.filter_clear": "Zurücksetzen",
|
||||
"files.saved_searches_label": "Gespeicherte Suchen",
|
||||
"files.saved_searches_empty": "Noch keine gespeicherten Suchen",
|
||||
"files.saved_searches_save": "Aktuelle speichern",
|
||||
"files.saved_searches_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"files.fulltext_search_label": "Volltextsuche",
|
||||
"files.fulltext_search_placeholder": "Dokumentinhalt, Absender, Tags, Typ durchsuchen...",
|
||||
"files.search_results_title": "Suchergebnisse",
|
||||
"files.search_results_empty": "Keine Ergebnisse gefunden.",
|
||||
"files.bulk_reprocess": "Ausgewählte erneut verarbeiten",
|
||||
"files.bulk_cloud_ocr": "Cloud-OCR erneut ausführen",
|
||||
"files.bulk_download": "Als ZIP herunterladen",
|
||||
"files.bulk_delete": "Ausgewählte löschen",
|
||||
"files.bulk_clear_selection": "Auswahl aufheben",
|
||||
"files.table_select_all": "Alle Dateien auf dieser Seite auswählen",
|
||||
"files.table_id": "ID",
|
||||
"files.table_original_filename": "Originaler Dateiname",
|
||||
"files.table_mime_type": "MIME-Typ",
|
||||
"files.table_created_at": "Erstellt am",
|
||||
"files.table_actions": "Aktionen",
|
||||
"files.table_empty": "Keine Dateien gefunden",
|
||||
"files.action_preview": "Schnellvorschau",
|
||||
"files.action_details": "Details anzeigen",
|
||||
"files.action_delete": "Datei löschen",
|
||||
"files.pagination_first": "Erste",
|
||||
"files.pagination_previous": "Vorherige",
|
||||
"files.pagination_next": "Nächste",
|
||||
"files.pagination_last": "Letzte",
|
||||
"files.delete_modal_title": "Löschung bestätigen",
|
||||
"files.delete_modal_message": "Sind Sie sicher, dass Sie diese Datei löschen möchten?",
|
||||
"files.delete_modal_cancel": "Abbrechen",
|
||||
"files.delete_modal_confirm": "Löschen",
|
||||
"files.preview_modal_title": "Vorschau",
|
||||
"files.preview_modal_close": "Vorschau schließen",
|
||||
"search.page_title": "Dokumente suchen",
|
||||
"search.heading": "Dokumentensuche",
|
||||
"search.input_placeholder": "Dokumente nach Inhalt, Absender, Tags, Typ suchen...",
|
||||
"search.button": "Suchen",
|
||||
"search.filter_document_type": "Dokumenttyp",
|
||||
"search.filter_document_type_placeholder": "z.B. Rechnung",
|
||||
"search.filter_tags_placeholder": "z.B. Amazon",
|
||||
"search.filter_sender": "Absender",
|
||||
"search.filter_sender_placeholder": "z.B. ACME GmbH",
|
||||
"search.filter_language": "Sprache",
|
||||
"search.filter_language_placeholder": "z.B. de",
|
||||
"search.filter_text_quality": "Textqualität",
|
||||
"search.filter_text_quality_all": "Alle",
|
||||
"search.filter_text_quality_high": "Hoch",
|
||||
"search.filter_text_quality_medium": "Mittel",
|
||||
"search.filter_text_quality_low": "Niedrig",
|
||||
"search.filter_text_quality_no_text": "Kein Text",
|
||||
"search.filter_date_from": "Datum von",
|
||||
"search.filter_date_to": "Datum bis",
|
||||
"search.filter_clear_button": "Filter zurücksetzen",
|
||||
"search.saved_label": "Gespeicherte Suchen",
|
||||
"search.saved_loading": "Laden...",
|
||||
"search.saved_empty": "Noch keine gespeicherten Suchen",
|
||||
"search.saved_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"search.saved_button": "Aktuelle speichern",
|
||||
"search.result_empty": "Keine Dokumente gefunden, die Ihrer Suche entsprechen.",
|
||||
"search.loading_indicator": "Suche läuft…",
|
||||
"search.error_message": "Suche ist vorübergehend nicht verfügbar. Bitte versuchen Sie es gleich erneut.",
|
||||
"help.page_title": "Hilfezentrum",
|
||||
"help.heading": "Hilfezentrum",
|
||||
"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.quickstart_heading": "Schnellstart",
|
||||
"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_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_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_heading": "Quellen – Dokumente einbringen",
|
||||
"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.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_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.destinations_heading": "Ziele – Wohin die Dokumente gehen",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-verknüpft. Dateien landen in Ihrem gewählten Ordner.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Dienstkonto oder OAuth. Unterstützt geteilte Laufwerke.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API-Integration.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Jeder S3-kompatible Bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Selbstgehosteter Cloud-Speicher über WebDAV.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Dokumente direkt in Paperless zur Archivierung übertragen.",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Sichere Dateiübertragung auf beliebige Server.",
|
||||
"help.destinations_email": "E-Mail-Weiterleitung",
|
||||
"help.destinations_email_desc": "Verarbeitete Dateien als SMTP-Anhänge gesendet.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "Metadaten per POST an einen externen Endpunkt senden.",
|
||||
"help.workflows_heading": "Arbeitsabläufe & Pipelines",
|
||||
"help.workflows_what_is": "Was ist eine Pipeline?",
|
||||
"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_typical_steps": "Typische Schritte",
|
||||
"help.workflows_step_1": "In PDF konvertieren",
|
||||
"help.workflows_step_2": "OCR – Text extrahieren",
|
||||
"help.workflows_step_3": "KI-Metadatenextraktion",
|
||||
"help.workflows_step_4": "An ein oder mehrere Ziele liefern",
|
||||
"help.workflows_creating": "Eine Pipeline erstellen",
|
||||
"help.workflows_step_1_create": "Gehen Sie im Hauptmenü zu Pipelines.",
|
||||
"help.workflows_step_2_create": "Klicken Sie auf Neue Pipeline und geben Sie ihr einen Namen.",
|
||||
"help.workflows_step_3_create": "Fügen Sie die benötigten Verarbeitungsschritte hinzu.",
|
||||
"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.faq_heading": "Häufig gestellte Fragen",
|
||||
"help.faq_1_q": "Wie lade ich Dokumente hoch?",
|
||||
"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_2_q": "Welche Dateiformate werden unterstützt?",
|
||||
"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_3_q": "Kann ich Dokumente per E-Mail importieren?",
|
||||
"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_4_q": "Wie funktionieren Verarbeitungs-Pipelines?",
|
||||
"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_5_q": "Sind meine Daten sicher?",
|
||||
"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.support_heading": "Support kontaktieren",
|
||||
"help.support_description": "Können Sie nicht finden, was Sie suchen? Unser Support-Team hilft Ihnen gerne weiter.",
|
||||
"help.support_admin_message": "Wenden Sie sich an Ihren Administrator für Support-Informationen.",
|
||||
"index.page_title_public": "Intelligente Dokumentenverarbeitung",
|
||||
"index.page_title_dashboard": "Übersicht",
|
||||
"index.badge_intelligent": "Intelligente Dokumentenverarbeitung",
|
||||
"index.hero_heading": "Vom Hochladen zur Erkenntnis – automatisch.",
|
||||
"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_signup": "Kostenlos starten",
|
||||
"index.hero_login": "Anmelden",
|
||||
"index.hero_pricing": "Tarife & Preise ansehen",
|
||||
"index.feature_section_title": "Alles, was Sie für intelligente Dokumenten-Workflows brauchen",
|
||||
"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_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_search": "Volltextsuche",
|
||||
"index.feature_search_desc": "Finden Sie sofort jedes Dokument nach Inhalt, Metadaten oder Tags in Ihrem gesamten Archiv.",
|
||||
"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.cta_heading": "Bereit, Ihren Dokumenten-Workflow zu verbessern?",
|
||||
"index.cta_description": "Schließen Sie sich Teams an, die ihre Dokumentenverarbeitung bereits mit DocuElevate automatisieren.",
|
||||
"index.cta_signup": "Kostenloses Konto erstellen",
|
||||
"index.cta_pricing": "Preise ansehen",
|
||||
"index.dashboard_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.platform_overview": "Plattformübersicht",
|
||||
"index.stat_total_files": "Dateien gesamt",
|
||||
"index.stat_files_today": "Dateien heute",
|
||||
"index.stat_files_month": "Dateien diesen Monat",
|
||||
"index.stat_active_users": "Aktive Benutzer",
|
||||
"index.usage_my_usage": "Meine Nutzung",
|
||||
"index.usage_lifetime": "Dateien gesamt",
|
||||
"index.usage_today": "Dateien heute",
|
||||
"index.usage_month": "Dateien diesen Monat",
|
||||
"index.usage_unlimited": "Unbegrenzt",
|
||||
"index.tier_plan": "Tarif",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "Alle Details ansehen",
|
||||
"index.quick_actions": "Schnellaktionen",
|
||||
"index.quick_upload": "Dokument hochladen",
|
||||
"index.quick_upload_desc": "Eine neue Datei verarbeiten",
|
||||
"index.quick_documents": "Meine Dokumente",
|
||||
"index.quick_documents_desc": "Ihre verarbeiteten Dateien durchsuchen",
|
||||
"index.quick_subscription": "Mein Abonnement",
|
||||
"index.quick_subscription_desc": "Tarif & Nutzungsdetails anzeigen",
|
||||
"index.quick_search": "Suche",
|
||||
"index.quick_search_desc": "Volltextsuche über Dokumente",
|
||||
"index.upgrade_plan": "Tarif upgraden",
|
||||
"index.upgrade_description": "Mehr Dokumente, mehr Ziele und Prioritäts-Support freischalten.",
|
||||
"index.upgrade_daily_limits": "Höhere tägliche & monatliche Limits",
|
||||
"index.upgrade_destinations": "Mehr Speicherziele",
|
||||
"index.upgrade_ocr_pages": "Mehr OCR-Seiten",
|
||||
"index.upgrade_view_pricing": "Tarife & Preise ansehen",
|
||||
"index.integrations_title": "Integrationen",
|
||||
"index.integrations_active": "Aktive Integrationen",
|
||||
"index.integrations_storage": "Speicherziele",
|
||||
"index.integrations_view_status": "Systemstatus anzeigen",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.capabilities_title": "Funktionen",
|
||||
"index.capabilities_ocr": "OCR & Metadatenextraktion mit KI",
|
||||
"index.capabilities_cloud": "Cloud-Speicher: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_paperless": "Paperless-ngx-Integration für Dokumentenverwaltung",
|
||||
"index.capabilities_ingestion": "E-Mail- & URL-basierte Dokumentenaufnahme",
|
||||
"index.capabilities_workflows": "Automatisierte Klassifizierung & Routing-Workflows",
|
||||
"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",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Ups, diese Seite konnten wir nicht finden!",
|
||||
"error.404_message": "Es scheint, als hätte DocuElevate das gesuchte Dokument verlegt. Keine Sorge – wir helfen Ihnen weiter.",
|
||||
"error.404_home": "Zur Startseite",
|
||||
"error.500_code": "500",
|
||||
"error.500_heading": "Ups! Etwas ist schiefgelaufen.",
|
||||
"error.500_description": "Unsere Server haben ein Problem und brauchen einen Moment.",
|
||||
"error.500_home": "Zur Startseite",
|
||||
"pipelines.page_title": "Verarbeitungs-Pipelines",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.default_label": "Standard",
|
||||
"pipelines.inactive_label": "Inaktiv",
|
||||
"pipelines.disabled_label": "Deaktiviert",
|
||||
"pipelines.enabled_label": "Aktiviert",
|
||||
"pipelines.empty_state": "Noch keine Pipelines",
|
||||
"pipelines.set_default": "Als meine Standard-Pipeline festlegen",
|
||||
"pipelines.description_label": "Beschreibung",
|
||||
"pipelines.active_label": "Aktiv",
|
||||
"integrations.page_title": "Integrationen",
|
||||
"integrations.imap_settings": "IMAP-Einstellungen",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.username_label": "Benutzername",
|
||||
"integrations.password_label": "Passwort",
|
||||
"integrations.folder_label": "Ordner",
|
||||
"integrations.empty_state": "Keine Integrationen konfiguriert",
|
||||
"status.page_title": "Systemstatus",
|
||||
"status.app_version": "App-Version",
|
||||
"status.build_date": "Build-Datum",
|
||||
"status.last_check": "Letzte Prüfung",
|
||||
"status.container_id": "Container-ID",
|
||||
"status.git_commit": "Git-Commit",
|
||||
"status.setting_label": "Einstellung",
|
||||
"status.value_label": "Wert",
|
||||
"notifications.page_title": "Benachrichtigungen",
|
||||
"notifications.manage_desc": "Verwalten Sie Ihren Posteingang, Ziele und Ereignispräferenzen",
|
||||
"notifications.tab_inbox": "Posteingang",
|
||||
"notifications.tab_settings": "Einstellungen",
|
||||
"notifications.filter_all": "Alle",
|
||||
"notifications.filter_unread": "Nur ungelesene",
|
||||
"notifications.filter_read": "Nur gelesene",
|
||||
"notifications.mark_all_read_btn": "Alle als gelesen markieren",
|
||||
"auth.login_title": "Anmelden",
|
||||
"auth.signup_title": "Registrieren",
|
||||
"auth.forgot_password": "Passwort vergessen?",
|
||||
"auth.remember_me": "Angemeldet bleiben",
|
||||
"auth.email_label": "E-Mail",
|
||||
"auth.password_label": "Passwort",
|
||||
"auth.confirm_password": "Passwort bestätigen",
|
||||
"auth.username_label": "Benutzername",
|
||||
"auth.display_name_label": "Anzeigename",
|
||||
"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à"
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"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à"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Panel",
|
||||
"nav.upload": "Subir",
|
||||
"nav.files": "Archivos",
|
||||
"nav.search": "Buscar",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integraciones",
|
||||
"nav.help": "Ayuda",
|
||||
"nav.notifications": "Notificaciones",
|
||||
"nav.pricing": "Precios",
|
||||
"nav.about": "Acerca de",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Configuración",
|
||||
"nav.users": "Usuarios",
|
||||
"nav.plan_designer": "Diseñador de planes",
|
||||
"nav.credentials": "Credenciales",
|
||||
"nav.file_manager": "Gestor de archivos",
|
||||
"nav.duplicates": "Duplicados",
|
||||
"nav.similarity": "Similitud",
|
||||
"nav.queue_monitor": "Monitor de cola",
|
||||
"nav.scheduled_jobs": "Tareas programadas",
|
||||
"nav.backup_restore": "Copia de seguridad y restauración",
|
||||
"nav.status": "Estado",
|
||||
"nav.api_docs": "Documentación API",
|
||||
"nav.developer_docs": "Documentación para desarrolladores",
|
||||
"nav.dark_mode": "Modo oscuro",
|
||||
"nav.light_mode": "Modo claro",
|
||||
"nav.toggle_dark_mode": "Alternar modo oscuro",
|
||||
"nav.toggle_nav": "Alternar menú de navegación",
|
||||
"nav.open_main_menu": "Abrir menú principal",
|
||||
"nav.skip_to_content": "Ir al contenido principal",
|
||||
"nav.main_navigation": "Navegación principal",
|
||||
"nav.admin_menu": "Menú de administración",
|
||||
"nav.admin_actions": "Acciones de administración",
|
||||
"nav.help_center": "Centro de ayuda",
|
||||
|
||||
"auth.login": "Iniciar sesión",
|
||||
"auth.logout": "Cerrar sesión",
|
||||
"auth.signup": "Registrarse",
|
||||
"auth.my_account": "Mi cuenta",
|
||||
"auth.profile": "Perfil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacidad",
|
||||
"footer.imprint": "Aviso legal",
|
||||
"footer.terms": "Términos",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licencia",
|
||||
"footer.attributions": "Atribuciones",
|
||||
"footer.version": "Versión {version}",
|
||||
"footer.navigation": "Navegación del pie de página",
|
||||
|
||||
"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.policy_link": "Política de cookies",
|
||||
"cookie.privacy_link": "Aviso de privacidad",
|
||||
"cookie.accept": "Entendido",
|
||||
"cookie.notice_label": "Aviso de cookies",
|
||||
|
||||
"common.save": "Guardar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.delete": "Eliminar",
|
||||
"common.edit": "Editar",
|
||||
"common.close": "Cerrar",
|
||||
"common.confirm": "Confirmar",
|
||||
"common.back": "Atrás",
|
||||
"common.next": "Siguiente",
|
||||
"common.loading": "Cargando...",
|
||||
"common.error": "Error",
|
||||
"common.success": "Éxito",
|
||||
"common.warning": "Advertencia",
|
||||
"common.info": "Información",
|
||||
"common.yes": "Sí",
|
||||
"common.no": "No",
|
||||
"common.search": "Buscar",
|
||||
"common.filter": "Filtrar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.refresh": "Actualizar",
|
||||
"common.download": "Descargar",
|
||||
"common.actions": "Acciones",
|
||||
"common.details": "Detalles",
|
||||
"common.name": "Nombre",
|
||||
"common.description": "Descripción",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Estado",
|
||||
"common.date": "Fecha",
|
||||
"common.size": "Tamaño",
|
||||
"common.created": "Creado",
|
||||
"common.updated": "Actualizado",
|
||||
"common.enabled": "Habilitado",
|
||||
"common.disabled": "Deshabilitado",
|
||||
"common.active": "Activo",
|
||||
"common.inactive": "Inactivo",
|
||||
"common.all": "Todo",
|
||||
"common.none": "Ninguno",
|
||||
"common.select": "Seleccionar",
|
||||
"common.upload": "Subir",
|
||||
"common.processing": "Procesando",
|
||||
"common.completed": "Completado",
|
||||
"common.failed": "Fallido",
|
||||
"common.pending": "Pendiente",
|
||||
"common.retry": "Reintentar",
|
||||
"common.view": "Ver",
|
||||
"common.copy": "Copiar",
|
||||
"common.copied": "¡Copiado!",
|
||||
|
||||
"language.selector": "Idioma",
|
||||
"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": "Idioma cambiado a {language}",
|
||||
|
||||
"dashboard.title": "Panel",
|
||||
"dashboard.total_files": "Total de archivos",
|
||||
"dashboard.files_today": "Archivos hoy",
|
||||
"dashboard.files_this_month": "Archivos este mes",
|
||||
"dashboard.ocr_processed": "OCR procesados",
|
||||
"dashboard.active_integrations": "Integraciones activas",
|
||||
"dashboard.storage_targets": "Destinos de almacenamiento",
|
||||
"dashboard.recent_activity": "Actividad reciente",
|
||||
"dashboard.quick_actions": "Acciones rápidas",
|
||||
"dashboard.welcome": "Bienvenido a DocuElevate",
|
||||
|
||||
"upload.title": "Subir documento",
|
||||
"upload.drag_drop": "Arrastre archivos aquí o haga clic para buscar",
|
||||
"upload.select_file": "Seleccionar archivo",
|
||||
"upload.uploading": "Subiendo...",
|
||||
"upload.success": "Archivo subido con éxito",
|
||||
"upload.error": "Error al subir",
|
||||
"upload.max_size": "Tamaño máximo del archivo: {size}",
|
||||
|
||||
"files.title": "Archivos",
|
||||
"files.no_files": "No se encontraron archivos",
|
||||
"files.filename": "Nombre del archivo",
|
||||
"files.document_title": "Título del documento",
|
||||
"files.uploaded": "Subido",
|
||||
"files.file_size": "Tamaño del archivo",
|
||||
"files.ocr_status": "Estado OCR",
|
||||
"files.tags": "Etiquetas",
|
||||
|
||||
"search.title": "Buscar documentos",
|
||||
"search.placeholder": "Buscar por nombre, contenido, etiquetas...",
|
||||
"search.no_results": "No se encontraron resultados",
|
||||
"search.results_count": "{count} resultados encontrados",
|
||||
|
||||
"settings.title": "Configuración",
|
||||
"settings.save_success": "Configuración guardada con éxito",
|
||||
"settings.save_error": "Error al guardar la configuración",
|
||||
"settings.reset_confirm": "¿Está seguro de que desea restablecer esta configuración?",
|
||||
|
||||
"integrations.title": "Integraciones",
|
||||
"integrations.connect": "Conectar",
|
||||
"integrations.disconnect": "Desconectar",
|
||||
"integrations.connected": "Conectado",
|
||||
"integrations.not_connected": "No conectado",
|
||||
"integrations.configure": "Configurar",
|
||||
|
||||
"pipelines.title": "Pipelines de procesamiento",
|
||||
"pipelines.create": "Crear pipeline",
|
||||
"pipelines.edit": "Editar pipeline",
|
||||
|
||||
"help.title": "Centro de ayuda",
|
||||
"help.getting_started": "Primeros pasos",
|
||||
"help.faq": "Preguntas frecuentes",
|
||||
"help.documentation": "Documentación",
|
||||
"help.support": "Soporte",
|
||||
|
||||
"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.",
|
||||
"error.forbidden": "Prohibido",
|
||||
"error.forbidden_message": "No tiene permiso para acceder a esta página.",
|
||||
|
||||
"notifications.title": "Notificaciones",
|
||||
"notifications.mark_read": "Marcar como leído",
|
||||
"notifications.mark_all_read": "Marcar todo como leído",
|
||||
"notifications.no_notifications": "Sin notificaciones",
|
||||
"notifications.unread_count": "{count} notificaciones no leídas"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Tableau de bord",
|
||||
"nav.upload": "Téléverser",
|
||||
"nav.files": "Fichiers",
|
||||
"nav.search": "Recherche",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Intégrations",
|
||||
"nav.help": "Aide",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.pricing": "Tarifs",
|
||||
"nav.about": "À propos",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Paramètres",
|
||||
"nav.users": "Utilisateurs",
|
||||
"nav.plan_designer": "Concepteur de plans",
|
||||
"nav.credentials": "Identifiants",
|
||||
"nav.file_manager": "Gestionnaire de fichiers",
|
||||
"nav.duplicates": "Doublons",
|
||||
"nav.similarity": "Similarité",
|
||||
"nav.queue_monitor": "File d'attente",
|
||||
"nav.scheduled_jobs": "Tâches planifiées",
|
||||
"nav.backup_restore": "Sauvegarde et restauration",
|
||||
"nav.status": "Statut",
|
||||
"nav.api_docs": "Documentation API",
|
||||
"nav.developer_docs": "Documentation développeur",
|
||||
"nav.dark_mode": "Mode sombre",
|
||||
"nav.light_mode": "Mode clair",
|
||||
"nav.toggle_dark_mode": "Basculer le mode sombre",
|
||||
"nav.toggle_nav": "Basculer le menu de navigation",
|
||||
"nav.open_main_menu": "Ouvrir le menu principal",
|
||||
"nav.skip_to_content": "Aller au contenu principal",
|
||||
"nav.main_navigation": "Navigation principale",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.admin_actions": "Actions admin",
|
||||
"nav.help_center": "Centre d'aide",
|
||||
|
||||
"auth.login": "Se connecter",
|
||||
"auth.logout": "Se déconnecter",
|
||||
"auth.signup": "S'inscrire",
|
||||
"auth.my_account": "Mon compte",
|
||||
"auth.profile": "Profil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Confidentialité",
|
||||
"footer.imprint": "Mentions légales",
|
||||
"footer.terms": "Conditions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licence",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.version": "Version {version}",
|
||||
"footer.navigation": "Navigation du pied de page",
|
||||
|
||||
"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.policy_link": "Politique de cookies",
|
||||
"cookie.privacy_link": "Avis de confidentialité",
|
||||
"cookie.accept": "Compris",
|
||||
"cookie.notice_label": "Avis relatif aux cookies",
|
||||
|
||||
"common.save": "Enregistrer",
|
||||
"common.cancel": "Annuler",
|
||||
"common.delete": "Supprimer",
|
||||
"common.edit": "Modifier",
|
||||
"common.close": "Fermer",
|
||||
"common.confirm": "Confirmer",
|
||||
"common.back": "Retour",
|
||||
"common.next": "Suivant",
|
||||
"common.loading": "Chargement...",
|
||||
"common.error": "Erreur",
|
||||
"common.success": "Succès",
|
||||
"common.warning": "Avertissement",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Oui",
|
||||
"common.no": "Non",
|
||||
"common.search": "Rechercher",
|
||||
"common.filter": "Filtrer",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.refresh": "Actualiser",
|
||||
"common.download": "Télécharger",
|
||||
"common.actions": "Actions",
|
||||
"common.details": "Détails",
|
||||
"common.name": "Nom",
|
||||
"common.description": "Description",
|
||||
"common.type": "Type",
|
||||
"common.status": "Statut",
|
||||
"common.date": "Date",
|
||||
"common.size": "Taille",
|
||||
"common.created": "Créé",
|
||||
"common.updated": "Mis à jour",
|
||||
"common.enabled": "Activé",
|
||||
"common.disabled": "Désactivé",
|
||||
"common.active": "Actif",
|
||||
"common.inactive": "Inactif",
|
||||
"common.all": "Tout",
|
||||
"common.none": "Aucun",
|
||||
"common.select": "Sélectionner",
|
||||
"common.upload": "Téléverser",
|
||||
"common.processing": "En cours de traitement",
|
||||
"common.completed": "Terminé",
|
||||
"common.failed": "Échoué",
|
||||
"common.pending": "En attente",
|
||||
"common.retry": "Réessayer",
|
||||
"common.view": "Voir",
|
||||
"common.copy": "Copier",
|
||||
"common.copied": "Copié !",
|
||||
|
||||
"language.selector": "Langue",
|
||||
"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": "Langue changée en {language}",
|
||||
|
||||
"dashboard.title": "Tableau de bord",
|
||||
"dashboard.total_files": "Total des fichiers",
|
||||
"dashboard.files_today": "Fichiers aujourd'hui",
|
||||
"dashboard.files_this_month": "Fichiers ce mois-ci",
|
||||
"dashboard.ocr_processed": "OCR traités",
|
||||
"dashboard.active_integrations": "Intégrations actives",
|
||||
"dashboard.storage_targets": "Destinations de stockage",
|
||||
"dashboard.recent_activity": "Activité récente",
|
||||
"dashboard.quick_actions": "Actions rapides",
|
||||
"dashboard.welcome": "Bienvenue sur DocuElevate",
|
||||
|
||||
"upload.title": "Téléverser un document",
|
||||
"upload.drag_drop": "Glissez-déposez vos fichiers ici ou cliquez pour parcourir",
|
||||
"upload.select_file": "Sélectionner un fichier",
|
||||
"upload.uploading": "Téléversement en cours...",
|
||||
"upload.success": "Fichier téléversé avec succès",
|
||||
"upload.error": "Échec du téléversement",
|
||||
"upload.max_size": "Taille maximale du fichier : {size}",
|
||||
|
||||
"files.title": "Fichiers",
|
||||
"files.no_files": "Aucun fichier trouvé",
|
||||
"files.filename": "Nom du fichier",
|
||||
"files.document_title": "Titre du document",
|
||||
"files.uploaded": "Téléversé",
|
||||
"files.file_size": "Taille du fichier",
|
||||
"files.ocr_status": "Statut OCR",
|
||||
"files.tags": "Étiquettes",
|
||||
|
||||
"search.title": "Rechercher des documents",
|
||||
"search.placeholder": "Rechercher par nom, contenu, étiquettes...",
|
||||
"search.no_results": "Aucun résultat trouvé",
|
||||
"search.results_count": "{count} résultats trouvés",
|
||||
|
||||
"settings.title": "Paramètres",
|
||||
"settings.save_success": "Paramètre enregistré avec succès",
|
||||
"settings.save_error": "Échec de l'enregistrement du paramètre",
|
||||
"settings.reset_confirm": "Êtes-vous sûr de vouloir réinitialiser ce paramètre ?",
|
||||
|
||||
"integrations.title": "Intégrations",
|
||||
"integrations.connect": "Connecter",
|
||||
"integrations.disconnect": "Déconnecter",
|
||||
"integrations.connected": "Connecté",
|
||||
"integrations.not_connected": "Non connecté",
|
||||
"integrations.configure": "Configurer",
|
||||
|
||||
"pipelines.title": "Pipelines de traitement",
|
||||
"pipelines.create": "Créer un pipeline",
|
||||
"pipelines.edit": "Modifier le pipeline",
|
||||
|
||||
"help.title": "Centre d'aide",
|
||||
"help.getting_started": "Premiers pas",
|
||||
"help.faq": "Questions fréquentes",
|
||||
"help.documentation": "Documentation",
|
||||
"help.support": "Support",
|
||||
|
||||
"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.",
|
||||
"error.forbidden": "Interdit",
|
||||
"error.forbidden_message": "Vous n'avez pas la permission d'accéder à cette page.",
|
||||
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.mark_read": "Marquer comme lu",
|
||||
"notifications.mark_all_read": "Tout marquer comme lu",
|
||||
"notifications.no_notifications": "Aucune notification",
|
||||
"notifications.unread_count": "{count} notifications non lues"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Cruscotto",
|
||||
"nav.upload": "Carica",
|
||||
"nav.files": "File",
|
||||
"nav.search": "Cerca",
|
||||
"nav.pipelines": "Pipeline",
|
||||
"nav.integrations": "Integrazioni",
|
||||
"nav.help": "Aiuto",
|
||||
"nav.notifications": "Notifiche",
|
||||
"nav.pricing": "Prezzi",
|
||||
"nav.about": "Informazioni",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Impostazioni",
|
||||
"nav.users": "Utenti",
|
||||
"nav.plan_designer": "Designer dei piani",
|
||||
"nav.credentials": "Credenziali",
|
||||
"nav.file_manager": "Gestore file",
|
||||
"nav.duplicates": "Duplicati",
|
||||
"nav.similarity": "Similarità",
|
||||
"nav.queue_monitor": "Monitor coda",
|
||||
"nav.scheduled_jobs": "Attività pianificate",
|
||||
"nav.backup_restore": "Backup e ripristino",
|
||||
"nav.status": "Stato",
|
||||
"nav.api_docs": "Documentazione API",
|
||||
"nav.developer_docs": "Documentazione sviluppatore",
|
||||
"nav.dark_mode": "Modalità scura",
|
||||
"nav.light_mode": "Modalità chiara",
|
||||
"nav.toggle_dark_mode": "Attiva/disattiva modalità scura",
|
||||
"nav.toggle_nav": "Attiva/disattiva menu di navigazione",
|
||||
"nav.open_main_menu": "Apri menu principale",
|
||||
"nav.skip_to_content": "Vai al contenuto principale",
|
||||
"nav.main_navigation": "Navigazione principale",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.admin_actions": "Azioni admin",
|
||||
"nav.help_center": "Centro assistenza",
|
||||
|
||||
"auth.login": "Accedi",
|
||||
"auth.logout": "Esci",
|
||||
"auth.signup": "Registrati",
|
||||
"auth.my_account": "Il mio account",
|
||||
"auth.profile": "Profilo",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Note legali",
|
||||
"footer.terms": "Termini",
|
||||
"footer.cookies": "Cookie",
|
||||
"footer.license": "Licenza",
|
||||
"footer.attributions": "Attribuzioni",
|
||||
"footer.version": "Versione {version}",
|
||||
"footer.navigation": "Navigazione a piè di pagina",
|
||||
|
||||
"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.policy_link": "Politica sui cookie",
|
||||
"cookie.privacy_link": "Informativa sulla privacy",
|
||||
"cookie.accept": "Ho capito",
|
||||
"cookie.notice_label": "Avviso sui cookie",
|
||||
|
||||
"common.save": "Salva",
|
||||
"common.cancel": "Annulla",
|
||||
"common.delete": "Elimina",
|
||||
"common.edit": "Modifica",
|
||||
"common.close": "Chiudi",
|
||||
"common.confirm": "Conferma",
|
||||
"common.back": "Indietro",
|
||||
"common.next": "Avanti",
|
||||
"common.loading": "Caricamento...",
|
||||
"common.error": "Errore",
|
||||
"common.success": "Successo",
|
||||
"common.warning": "Avviso",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Sì",
|
||||
"common.no": "No",
|
||||
"common.search": "Cerca",
|
||||
"common.filter": "Filtra",
|
||||
"common.reset": "Reimposta",
|
||||
"common.refresh": "Aggiorna",
|
||||
"common.download": "Scarica",
|
||||
"common.actions": "Azioni",
|
||||
"common.details": "Dettagli",
|
||||
"common.name": "Nome",
|
||||
"common.description": "Descrizione",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Stato",
|
||||
"common.date": "Data",
|
||||
"common.size": "Dimensione",
|
||||
"common.created": "Creato",
|
||||
"common.updated": "Aggiornato",
|
||||
"common.enabled": "Abilitato",
|
||||
"common.disabled": "Disabilitato",
|
||||
"common.active": "Attivo",
|
||||
"common.inactive": "Inattivo",
|
||||
"common.all": "Tutto",
|
||||
"common.none": "Nessuno",
|
||||
"common.select": "Seleziona",
|
||||
"common.upload": "Carica",
|
||||
"common.processing": "In elaborazione",
|
||||
"common.completed": "Completato",
|
||||
"common.failed": "Fallito",
|
||||
"common.pending": "In attesa",
|
||||
"common.retry": "Riprova",
|
||||
"common.view": "Visualizza",
|
||||
"common.copy": "Copia",
|
||||
"common.copied": "Copiato!",
|
||||
|
||||
"language.selector": "Lingua",
|
||||
"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": "Lingua cambiata in {language}",
|
||||
|
||||
"dashboard.title": "Cruscotto",
|
||||
"dashboard.total_files": "File totali",
|
||||
"dashboard.files_today": "File oggi",
|
||||
"dashboard.files_this_month": "File questo mese",
|
||||
"dashboard.ocr_processed": "OCR elaborati",
|
||||
"dashboard.active_integrations": "Integrazioni attive",
|
||||
"dashboard.storage_targets": "Destinazioni di archiviazione",
|
||||
"dashboard.recent_activity": "Attività recente",
|
||||
"dashboard.quick_actions": "Azioni rapide",
|
||||
"dashboard.welcome": "Benvenuto su DocuElevate",
|
||||
|
||||
"upload.title": "Carica documento",
|
||||
"upload.drag_drop": "Trascina i file qui o fai clic per sfogliare",
|
||||
"upload.select_file": "Seleziona file",
|
||||
"upload.uploading": "Caricamento in corso...",
|
||||
"upload.success": "File caricato con successo",
|
||||
"upload.error": "Caricamento fallito",
|
||||
"upload.max_size": "Dimensione massima del file: {size}",
|
||||
|
||||
"files.title": "File",
|
||||
"files.no_files": "Nessun file trovato",
|
||||
"files.filename": "Nome del file",
|
||||
"files.document_title": "Titolo del documento",
|
||||
"files.uploaded": "Caricato",
|
||||
"files.file_size": "Dimensione del file",
|
||||
"files.ocr_status": "Stato OCR",
|
||||
"files.tags": "Tag",
|
||||
|
||||
"search.title": "Cerca documenti",
|
||||
"search.placeholder": "Cerca per nome, contenuto, tag...",
|
||||
"search.no_results": "Nessun risultato trovato",
|
||||
"search.results_count": "{count} risultati trovati",
|
||||
|
||||
"settings.title": "Impostazioni",
|
||||
"settings.save_success": "Impostazione salvata con successo",
|
||||
"settings.save_error": "Salvataggio impostazione fallito",
|
||||
"settings.reset_confirm": "Sei sicuro di voler reimpostare questa impostazione?",
|
||||
|
||||
"integrations.title": "Integrazioni",
|
||||
"integrations.connect": "Connetti",
|
||||
"integrations.disconnect": "Disconnetti",
|
||||
"integrations.connected": "Connesso",
|
||||
"integrations.not_connected": "Non connesso",
|
||||
"integrations.configure": "Configura",
|
||||
|
||||
"pipelines.title": "Pipeline di elaborazione",
|
||||
"pipelines.create": "Crea pipeline",
|
||||
"pipelines.edit": "Modifica pipeline",
|
||||
|
||||
"help.title": "Centro assistenza",
|
||||
"help.getting_started": "Per iniziare",
|
||||
"help.faq": "Domande frequenti",
|
||||
"help.documentation": "Documentazione",
|
||||
"help.support": "Supporto",
|
||||
|
||||
"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.",
|
||||
"error.forbidden": "Vietato",
|
||||
"error.forbidden_message": "Non hai il permesso di accedere a questa pagina.",
|
||||
|
||||
"notifications.title": "Notifiche",
|
||||
"notifications.mark_read": "Segna come letto",
|
||||
"notifications.mark_all_read": "Segna tutto come letto",
|
||||
"notifications.no_notifications": "Nessuna notifica",
|
||||
"notifications.unread_count": "{count} notifiche non lette"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Uploaden",
|
||||
"nav.files": "Bestanden",
|
||||
"nav.search": "Zoeken",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integraties",
|
||||
"nav.help": "Help",
|
||||
"nav.notifications": "Meldingen",
|
||||
"nav.pricing": "Prijzen",
|
||||
"nav.about": "Over ons",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Instellingen",
|
||||
"nav.users": "Gebruikers",
|
||||
"nav.plan_designer": "Planontwerper",
|
||||
"nav.credentials": "Referenties",
|
||||
"nav.file_manager": "Bestandsbeheer",
|
||||
"nav.duplicates": "Duplicaten",
|
||||
"nav.similarity": "Gelijkenis",
|
||||
"nav.queue_monitor": "Wachtrijmonitor",
|
||||
"nav.scheduled_jobs": "Geplande taken",
|
||||
"nav.backup_restore": "Back-up en herstel",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "API-documentatie",
|
||||
"nav.developer_docs": "Ontwikkelaarsdocumentatie",
|
||||
"nav.dark_mode": "Donkere modus",
|
||||
"nav.light_mode": "Lichte modus",
|
||||
"nav.toggle_dark_mode": "Donkere modus schakelen",
|
||||
"nav.toggle_nav": "Navigatiemenu schakelen",
|
||||
"nav.open_main_menu": "Hoofdmenu openen",
|
||||
"nav.skip_to_content": "Ga naar hoofdinhoud",
|
||||
"nav.main_navigation": "Hoofdnavigatie",
|
||||
"nav.admin_menu": "Admin-menu",
|
||||
"nav.admin_actions": "Admin-acties",
|
||||
"nav.help_center": "Helpcentrum",
|
||||
|
||||
"auth.login": "Inloggen",
|
||||
"auth.logout": "Uitloggen",
|
||||
"auth.signup": "Registreren",
|
||||
"auth.my_account": "Mijn account",
|
||||
"auth.profile": "Profiel",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Colofon",
|
||||
"footer.terms": "Voorwaarden",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licentie",
|
||||
"footer.attributions": "Attributies",
|
||||
"footer.version": "Versie {version}",
|
||||
"footer.navigation": "Voettekstnavigatie",
|
||||
|
||||
"cookie.notice": "DocuElevate gebruikt alleen essentiële sessiecookies die nodig zijn voor authenticatie en werking van de service. Er worden geen tracking- of analysecookies gebruikt.",
|
||||
"cookie.policy_link": "Cookiebeleid",
|
||||
"cookie.privacy_link": "Privacyverklaring",
|
||||
"cookie.accept": "Begrepen",
|
||||
"cookie.notice_label": "Cookiemelding",
|
||||
|
||||
"common.save": "Opslaan",
|
||||
"common.cancel": "Annuleren",
|
||||
"common.delete": "Verwijderen",
|
||||
"common.edit": "Bewerken",
|
||||
"common.close": "Sluiten",
|
||||
"common.confirm": "Bevestigen",
|
||||
"common.back": "Terug",
|
||||
"common.next": "Volgende",
|
||||
"common.loading": "Laden...",
|
||||
"common.error": "Fout",
|
||||
"common.success": "Succes",
|
||||
"common.warning": "Waarschuwing",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Ja",
|
||||
"common.no": "Nee",
|
||||
"common.search": "Zoeken",
|
||||
"common.filter": "Filteren",
|
||||
"common.reset": "Herstellen",
|
||||
"common.refresh": "Vernieuwen",
|
||||
"common.download": "Downloaden",
|
||||
"common.actions": "Acties",
|
||||
"common.details": "Details",
|
||||
"common.name": "Naam",
|
||||
"common.description": "Beschrijving",
|
||||
"common.type": "Type",
|
||||
"common.status": "Status",
|
||||
"common.date": "Datum",
|
||||
"common.size": "Grootte",
|
||||
"common.created": "Aangemaakt",
|
||||
"common.updated": "Bijgewerkt",
|
||||
"common.enabled": "Ingeschakeld",
|
||||
"common.disabled": "Uitgeschakeld",
|
||||
"common.active": "Actief",
|
||||
"common.inactive": "Inactief",
|
||||
"common.all": "Alles",
|
||||
"common.none": "Geen",
|
||||
"common.select": "Selecteren",
|
||||
"common.upload": "Uploaden",
|
||||
"common.processing": "Verwerken",
|
||||
"common.completed": "Voltooid",
|
||||
"common.failed": "Mislukt",
|
||||
"common.pending": "In afwachting",
|
||||
"common.retry": "Opnieuw proberen",
|
||||
"common.view": "Bekijken",
|
||||
"common.copy": "Kopiëren",
|
||||
"common.copied": "Gekopieerd!",
|
||||
|
||||
"language.selector": "Taal",
|
||||
"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": "Taal gewijzigd naar {language}",
|
||||
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Totaal bestanden",
|
||||
"dashboard.files_today": "Bestanden vandaag",
|
||||
"dashboard.files_this_month": "Bestanden deze maand",
|
||||
"dashboard.ocr_processed": "OCR verwerkt",
|
||||
"dashboard.active_integrations": "Actieve integraties",
|
||||
"dashboard.storage_targets": "Opslagdoelen",
|
||||
"dashboard.recent_activity": "Recente activiteit",
|
||||
"dashboard.quick_actions": "Snelle acties",
|
||||
"dashboard.welcome": "Welkom bij DocuElevate",
|
||||
|
||||
"upload.title": "Document uploaden",
|
||||
"upload.drag_drop": "Sleep bestanden hierheen of klik om te bladeren",
|
||||
"upload.select_file": "Bestand selecteren",
|
||||
"upload.uploading": "Uploaden...",
|
||||
"upload.success": "Bestand succesvol geüpload",
|
||||
"upload.error": "Upload mislukt",
|
||||
"upload.max_size": "Maximale bestandsgrootte: {size}",
|
||||
|
||||
"files.title": "Bestanden",
|
||||
"files.no_files": "Geen bestanden gevonden",
|
||||
"files.filename": "Bestandsnaam",
|
||||
"files.document_title": "Documenttitel",
|
||||
"files.uploaded": "Geüpload",
|
||||
"files.file_size": "Bestandsgrootte",
|
||||
"files.ocr_status": "OCR-status",
|
||||
"files.tags": "Tags",
|
||||
|
||||
"search.title": "Documenten zoeken",
|
||||
"search.placeholder": "Zoeken op naam, inhoud, tags...",
|
||||
"search.no_results": "Geen resultaten gevonden",
|
||||
"search.results_count": "{count} resultaten gevonden",
|
||||
|
||||
"settings.title": "Instellingen",
|
||||
"settings.save_success": "Instelling succesvol opgeslagen",
|
||||
"settings.save_error": "Instelling opslaan mislukt",
|
||||
"settings.reset_confirm": "Weet u zeker dat u deze instelling wilt herstellen?",
|
||||
|
||||
"integrations.title": "Integraties",
|
||||
"integrations.connect": "Verbinden",
|
||||
"integrations.disconnect": "Verbreken",
|
||||
"integrations.connected": "Verbonden",
|
||||
"integrations.not_connected": "Niet verbonden",
|
||||
"integrations.configure": "Configureren",
|
||||
|
||||
"pipelines.title": "Verwerkingspipelines",
|
||||
"pipelines.create": "Pipeline maken",
|
||||
"pipelines.edit": "Pipeline bewerken",
|
||||
|
||||
"help.title": "Helpcentrum",
|
||||
"help.getting_started": "Aan de slag",
|
||||
"help.faq": "Veelgestelde vragen",
|
||||
"help.documentation": "Documentatie",
|
||||
"help.support": "Ondersteuning",
|
||||
|
||||
"error.not_found": "Pagina niet gevonden",
|
||||
"error.not_found_message": "De pagina die u zoekt bestaat niet.",
|
||||
"error.server_error": "Interne serverfout",
|
||||
"error.server_error_message": "Er is iets misgegaan. Probeer het later opnieuw.",
|
||||
"error.unauthorized": "Niet geautoriseerd",
|
||||
"error.unauthorized_message": "U moet inloggen om deze pagina te openen.",
|
||||
"error.forbidden": "Verboden",
|
||||
"error.forbidden_message": "U heeft geen toestemming om deze pagina te openen.",
|
||||
|
||||
"notifications.title": "Meldingen",
|
||||
"notifications.mark_read": "Markeren als gelezen",
|
||||
"notifications.mark_all_read": "Alles als gelezen markeren",
|
||||
"notifications.no_notifications": "Geen meldingen",
|
||||
"notifications.unread_count": "{count} ongelezen meldingen"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Pulpit",
|
||||
"nav.upload": "Prześlij",
|
||||
"nav.files": "Pliki",
|
||||
"nav.search": "Szukaj",
|
||||
"nav.pipelines": "Potoki",
|
||||
"nav.integrations": "Integracje",
|
||||
"nav.help": "Pomoc",
|
||||
"nav.notifications": "Powiadomienia",
|
||||
"nav.pricing": "Cennik",
|
||||
"nav.about": "O nas",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Ustawienia",
|
||||
"nav.users": "Użytkownicy",
|
||||
"nav.plan_designer": "Projektant planów",
|
||||
"nav.credentials": "Poświadczenia",
|
||||
"nav.file_manager": "Menedżer plików",
|
||||
"nav.duplicates": "Duplikaty",
|
||||
"nav.similarity": "Podobieństwo",
|
||||
"nav.queue_monitor": "Monitor kolejki",
|
||||
"nav.scheduled_jobs": "Zaplanowane zadania",
|
||||
"nav.backup_restore": "Kopia zapasowa i przywracanie",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "Dokumentacja API",
|
||||
"nav.developer_docs": "Dokumentacja dla programistów",
|
||||
"nav.dark_mode": "Tryb ciemny",
|
||||
"nav.light_mode": "Tryb jasny",
|
||||
"nav.toggle_dark_mode": "Przełącz tryb ciemny",
|
||||
"nav.toggle_nav": "Przełącz menu nawigacji",
|
||||
"nav.open_main_menu": "Otwórz menu główne",
|
||||
"nav.skip_to_content": "Przejdź do treści głównej",
|
||||
"nav.main_navigation": "Nawigacja główna",
|
||||
"nav.admin_menu": "Menu administratora",
|
||||
"nav.admin_actions": "Akcje administratora",
|
||||
"nav.help_center": "Centrum pomocy",
|
||||
|
||||
"auth.login": "Zaloguj się",
|
||||
"auth.logout": "Wyloguj się",
|
||||
"auth.signup": "Zarejestruj się",
|
||||
"auth.my_account": "Moje konto",
|
||||
"auth.profile": "Profil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Prywatność",
|
||||
"footer.imprint": "Impressum",
|
||||
"footer.terms": "Regulamin",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licencja",
|
||||
"footer.attributions": "Atrybuty",
|
||||
"footer.version": "Wersja {version}",
|
||||
"footer.navigation": "Nawigacja stopki",
|
||||
|
||||
"cookie.notice": "DocuElevate używa wyłącznie niezbędnych plików cookie sesji wymaganych do uwierzytelniania i działania usługi. Nie są używane pliki cookie śledzące ani analityczne.",
|
||||
"cookie.policy_link": "Polityka plików cookie",
|
||||
"cookie.privacy_link": "Informacja o prywatności",
|
||||
"cookie.accept": "Rozumiem",
|
||||
"cookie.notice_label": "Informacja o plikach cookie",
|
||||
|
||||
"common.save": "Zapisz",
|
||||
"common.cancel": "Anuluj",
|
||||
"common.delete": "Usuń",
|
||||
"common.edit": "Edytuj",
|
||||
"common.close": "Zamknij",
|
||||
"common.confirm": "Potwierdź",
|
||||
"common.back": "Wstecz",
|
||||
"common.next": "Dalej",
|
||||
"common.loading": "Ładowanie...",
|
||||
"common.error": "Błąd",
|
||||
"common.success": "Sukces",
|
||||
"common.warning": "Ostrzeżenie",
|
||||
"common.info": "Informacja",
|
||||
"common.yes": "Tak",
|
||||
"common.no": "Nie",
|
||||
"common.search": "Szukaj",
|
||||
"common.filter": "Filtruj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.refresh": "Odśwież",
|
||||
"common.download": "Pobierz",
|
||||
"common.actions": "Akcje",
|
||||
"common.details": "Szczegóły",
|
||||
"common.name": "Nazwa",
|
||||
"common.description": "Opis",
|
||||
"common.type": "Typ",
|
||||
"common.status": "Status",
|
||||
"common.date": "Data",
|
||||
"common.size": "Rozmiar",
|
||||
"common.created": "Utworzono",
|
||||
"common.updated": "Zaktualizowano",
|
||||
"common.enabled": "Włączony",
|
||||
"common.disabled": "Wyłączony",
|
||||
"common.active": "Aktywny",
|
||||
"common.inactive": "Nieaktywny",
|
||||
"common.all": "Wszystko",
|
||||
"common.none": "Brak",
|
||||
"common.select": "Wybierz",
|
||||
"common.upload": "Prześlij",
|
||||
"common.processing": "Przetwarzanie",
|
||||
"common.completed": "Zakończono",
|
||||
"common.failed": "Nieudane",
|
||||
"common.pending": "Oczekujące",
|
||||
"common.retry": "Ponów",
|
||||
"common.view": "Wyświetl",
|
||||
"common.copy": "Kopiuj",
|
||||
"common.copied": "Skopiowano!",
|
||||
|
||||
"language.selector": "Język",
|
||||
"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": "Język zmieniony na {language}",
|
||||
|
||||
"dashboard.title": "Pulpit",
|
||||
"dashboard.total_files": "Pliki ogółem",
|
||||
"dashboard.files_today": "Pliki dzisiaj",
|
||||
"dashboard.files_this_month": "Pliki w tym miesiącu",
|
||||
"dashboard.ocr_processed": "OCR przetworzone",
|
||||
"dashboard.active_integrations": "Aktywne integracje",
|
||||
"dashboard.storage_targets": "Cele przechowywania",
|
||||
"dashboard.recent_activity": "Ostatnia aktywność",
|
||||
"dashboard.quick_actions": "Szybkie akcje",
|
||||
"dashboard.welcome": "Witamy w DocuElevate",
|
||||
|
||||
"upload.title": "Prześlij dokument",
|
||||
"upload.drag_drop": "Przeciągnij pliki tutaj lub kliknij, aby przeglądać",
|
||||
"upload.select_file": "Wybierz plik",
|
||||
"upload.uploading": "Przesyłanie...",
|
||||
"upload.success": "Plik przesłany pomyślnie",
|
||||
"upload.error": "Przesyłanie nie powiodło się",
|
||||
"upload.max_size": "Maksymalny rozmiar pliku: {size}",
|
||||
|
||||
"files.title": "Pliki",
|
||||
"files.no_files": "Nie znaleziono plików",
|
||||
"files.filename": "Nazwa pliku",
|
||||
"files.document_title": "Tytuł dokumentu",
|
||||
"files.uploaded": "Przesłano",
|
||||
"files.file_size": "Rozmiar pliku",
|
||||
"files.ocr_status": "Status OCR",
|
||||
"files.tags": "Tagi",
|
||||
|
||||
"search.title": "Szukaj dokumentów",
|
||||
"search.placeholder": "Szukaj wg nazwy, treści, tagów...",
|
||||
"search.no_results": "Nie znaleziono wyników",
|
||||
"search.results_count": "Znaleziono {count} wyników",
|
||||
|
||||
"settings.title": "Ustawienia",
|
||||
"settings.save_success": "Ustawienie zapisane pomyślnie",
|
||||
"settings.save_error": "Nie udało się zapisać ustawienia",
|
||||
"settings.reset_confirm": "Czy na pewno chcesz zresetować to ustawienie?",
|
||||
|
||||
"integrations.title": "Integracje",
|
||||
"integrations.connect": "Połącz",
|
||||
"integrations.disconnect": "Rozłącz",
|
||||
"integrations.connected": "Połączono",
|
||||
"integrations.not_connected": "Nie połączono",
|
||||
"integrations.configure": "Konfiguruj",
|
||||
|
||||
"pipelines.title": "Potoki przetwarzania",
|
||||
"pipelines.create": "Utwórz potok",
|
||||
"pipelines.edit": "Edytuj potok",
|
||||
|
||||
"help.title": "Centrum pomocy",
|
||||
"help.getting_started": "Pierwsze kroki",
|
||||
"help.faq": "Często zadawane pytania",
|
||||
"help.documentation": "Dokumentacja",
|
||||
"help.support": "Wsparcie",
|
||||
|
||||
"error.not_found": "Nie znaleziono strony",
|
||||
"error.not_found_message": "Szukana strona nie istnieje.",
|
||||
"error.server_error": "Wewnętrzny błąd serwera",
|
||||
"error.server_error_message": "Coś poszło nie tak. Spróbuj ponownie później.",
|
||||
"error.unauthorized": "Brak autoryzacji",
|
||||
"error.unauthorized_message": "Musisz się zalogować, aby uzyskać dostęp do tej strony.",
|
||||
"error.forbidden": "Zabroniono",
|
||||
"error.forbidden_message": "Nie masz uprawnień do dostępu do tej strony.",
|
||||
|
||||
"notifications.title": "Powiadomienia",
|
||||
"notifications.mark_read": "Oznacz jako przeczytane",
|
||||
"notifications.mark_all_read": "Oznacz wszystkie jako przeczytane",
|
||||
"notifications.no_notifications": "Brak powiadomień",
|
||||
"notifications.unread_count": "{count} nieprzeczytanych powiadomień"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Painel",
|
||||
"nav.upload": "Carregar",
|
||||
"nav.files": "Ficheiros",
|
||||
"nav.search": "Pesquisar",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integrações",
|
||||
"nav.help": "Ajuda",
|
||||
"nav.notifications": "Notificações",
|
||||
"nav.pricing": "Preços",
|
||||
"nav.about": "Sobre",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Definições",
|
||||
"nav.users": "Utilizadores",
|
||||
"nav.plan_designer": "Designer de planos",
|
||||
"nav.credentials": "Credenciais",
|
||||
"nav.file_manager": "Gestor de ficheiros",
|
||||
"nav.duplicates": "Duplicados",
|
||||
"nav.similarity": "Similaridade",
|
||||
"nav.queue_monitor": "Monitor de fila",
|
||||
"nav.scheduled_jobs": "Tarefas agendadas",
|
||||
"nav.backup_restore": "Cópia de segurança e restauro",
|
||||
"nav.status": "Estado",
|
||||
"nav.api_docs": "Documentação API",
|
||||
"nav.developer_docs": "Documentação para programadores",
|
||||
"nav.dark_mode": "Modo escuro",
|
||||
"nav.light_mode": "Modo claro",
|
||||
"nav.toggle_dark_mode": "Alternar modo escuro",
|
||||
"nav.toggle_nav": "Alternar menu de navegação",
|
||||
"nav.open_main_menu": "Abrir menu principal",
|
||||
"nav.skip_to_content": "Ir para o conteúdo principal",
|
||||
"nav.main_navigation": "Navegação principal",
|
||||
"nav.admin_menu": "Menu de administração",
|
||||
"nav.admin_actions": "Ações de administração",
|
||||
"nav.help_center": "Centro de ajuda",
|
||||
|
||||
"auth.login": "Iniciar sessão",
|
||||
"auth.logout": "Terminar sessão",
|
||||
"auth.signup": "Registar",
|
||||
"auth.my_account": "A minha conta",
|
||||
"auth.profile": "Perfil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacidade",
|
||||
"footer.imprint": "Aviso legal",
|
||||
"footer.terms": "Termos",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licença",
|
||||
"footer.attributions": "Atribuições",
|
||||
"footer.version": "Versão {version}",
|
||||
"footer.navigation": "Navegação do rodapé",
|
||||
|
||||
"cookie.notice": "O DocuElevate utiliza apenas cookies de sessão essenciais necessários para a autenticação e o funcionamento do serviço. Não são utilizados cookies de rastreamento ou analíticos.",
|
||||
"cookie.policy_link": "Política de cookies",
|
||||
"cookie.privacy_link": "Aviso de privacidade",
|
||||
"cookie.accept": "Entendido",
|
||||
"cookie.notice_label": "Aviso de cookies",
|
||||
|
||||
"common.save": "Guardar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.delete": "Eliminar",
|
||||
"common.edit": "Editar",
|
||||
"common.close": "Fechar",
|
||||
"common.confirm": "Confirmar",
|
||||
"common.back": "Voltar",
|
||||
"common.next": "Seguinte",
|
||||
"common.loading": "A carregar...",
|
||||
"common.error": "Erro",
|
||||
"common.success": "Sucesso",
|
||||
"common.warning": "Aviso",
|
||||
"common.info": "Informação",
|
||||
"common.yes": "Sim",
|
||||
"common.no": "Não",
|
||||
"common.search": "Pesquisar",
|
||||
"common.filter": "Filtrar",
|
||||
"common.reset": "Repor",
|
||||
"common.refresh": "Atualizar",
|
||||
"common.download": "Descarregar",
|
||||
"common.actions": "Ações",
|
||||
"common.details": "Detalhes",
|
||||
"common.name": "Nome",
|
||||
"common.description": "Descrição",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Estado",
|
||||
"common.date": "Data",
|
||||
"common.size": "Tamanho",
|
||||
"common.created": "Criado",
|
||||
"common.updated": "Atualizado",
|
||||
"common.enabled": "Ativado",
|
||||
"common.disabled": "Desativado",
|
||||
"common.active": "Ativo",
|
||||
"common.inactive": "Inativo",
|
||||
"common.all": "Tudo",
|
||||
"common.none": "Nenhum",
|
||||
"common.select": "Selecionar",
|
||||
"common.upload": "Carregar",
|
||||
"common.processing": "A processar",
|
||||
"common.completed": "Concluído",
|
||||
"common.failed": "Falhado",
|
||||
"common.pending": "Pendente",
|
||||
"common.retry": "Tentar novamente",
|
||||
"common.view": "Ver",
|
||||
"common.copy": "Copiar",
|
||||
"common.copied": "Copiado!",
|
||||
|
||||
"language.selector": "Idioma",
|
||||
"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": "Idioma alterado para {language}",
|
||||
|
||||
"dashboard.title": "Painel",
|
||||
"dashboard.total_files": "Total de ficheiros",
|
||||
"dashboard.files_today": "Ficheiros hoje",
|
||||
"dashboard.files_this_month": "Ficheiros este mês",
|
||||
"dashboard.ocr_processed": "OCR processados",
|
||||
"dashboard.active_integrations": "Integrações ativas",
|
||||
"dashboard.storage_targets": "Destinos de armazenamento",
|
||||
"dashboard.recent_activity": "Atividade recente",
|
||||
"dashboard.quick_actions": "Ações rápidas",
|
||||
"dashboard.welcome": "Bem-vindo ao DocuElevate",
|
||||
|
||||
"upload.title": "Carregar documento",
|
||||
"upload.drag_drop": "Arraste ficheiros para aqui ou clique para procurar",
|
||||
"upload.select_file": "Selecionar ficheiro",
|
||||
"upload.uploading": "A carregar...",
|
||||
"upload.success": "Ficheiro carregado com sucesso",
|
||||
"upload.error": "Falha ao carregar",
|
||||
"upload.max_size": "Tamanho máximo do ficheiro: {size}",
|
||||
|
||||
"files.title": "Ficheiros",
|
||||
"files.no_files": "Nenhum ficheiro encontrado",
|
||||
"files.filename": "Nome do ficheiro",
|
||||
"files.document_title": "Título do documento",
|
||||
"files.uploaded": "Carregado",
|
||||
"files.file_size": "Tamanho do ficheiro",
|
||||
"files.ocr_status": "Estado OCR",
|
||||
"files.tags": "Etiquetas",
|
||||
|
||||
"search.title": "Pesquisar documentos",
|
||||
"search.placeholder": "Pesquisar por nome, conteúdo, etiquetas...",
|
||||
"search.no_results": "Nenhum resultado encontrado",
|
||||
"search.results_count": "{count} resultados encontrados",
|
||||
|
||||
"settings.title": "Definições",
|
||||
"settings.save_success": "Definição guardada com sucesso",
|
||||
"settings.save_error": "Falha ao guardar definição",
|
||||
"settings.reset_confirm": "Tem a certeza de que pretende repor esta definição?",
|
||||
|
||||
"integrations.title": "Integrações",
|
||||
"integrations.connect": "Ligar",
|
||||
"integrations.disconnect": "Desligar",
|
||||
"integrations.connected": "Ligado",
|
||||
"integrations.not_connected": "Não ligado",
|
||||
"integrations.configure": "Configurar",
|
||||
|
||||
"pipelines.title": "Pipelines de processamento",
|
||||
"pipelines.create": "Criar pipeline",
|
||||
"pipelines.edit": "Editar pipeline",
|
||||
|
||||
"help.title": "Centro de ajuda",
|
||||
"help.getting_started": "Primeiros passos",
|
||||
"help.faq": "Perguntas frequentes",
|
||||
"help.documentation": "Documentação",
|
||||
"help.support": "Suporte",
|
||||
|
||||
"error.not_found": "Página não encontrada",
|
||||
"error.not_found_message": "A página que procura não existe.",
|
||||
"error.server_error": "Erro interno do servidor",
|
||||
"error.server_error_message": "Algo correu mal. Tente novamente mais tarde.",
|
||||
"error.unauthorized": "Não autorizado",
|
||||
"error.unauthorized_message": "Precisa de iniciar sessão para aceder a esta página.",
|
||||
"error.forbidden": "Proibido",
|
||||
"error.forbidden_message": "Não tem permissão para aceder a esta página.",
|
||||
|
||||
"notifications.title": "Notificações",
|
||||
"notifications.mark_read": "Marcar como lida",
|
||||
"notifications.mark_all_read": "Marcar todas como lidas",
|
||||
"notifications.no_notifications": "Sem notificações",
|
||||
"notifications.unread_count": "{count} notificações por ler"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Панель управления",
|
||||
"nav.upload": "Загрузить",
|
||||
"nav.files": "Файлы",
|
||||
"nav.search": "Поиск",
|
||||
"nav.pipelines": "Конвейеры",
|
||||
"nav.integrations": "Интеграции",
|
||||
"nav.help": "Помощь",
|
||||
"nav.notifications": "Уведомления",
|
||||
"nav.pricing": "Цены",
|
||||
"nav.about": "О нас",
|
||||
"nav.admin": "Админ",
|
||||
"nav.settings": "Настройки",
|
||||
"nav.users": "Пользователи",
|
||||
"nav.plan_designer": "Конструктор планов",
|
||||
"nav.credentials": "Учётные данные",
|
||||
"nav.file_manager": "Менеджер файлов",
|
||||
"nav.duplicates": "Дубликаты",
|
||||
"nav.similarity": "Сходство",
|
||||
"nav.queue_monitor": "Монитор очереди",
|
||||
"nav.scheduled_jobs": "Запланированные задачи",
|
||||
"nav.backup_restore": "Резервное копирование и восстановление",
|
||||
"nav.status": "Статус",
|
||||
"nav.api_docs": "Документация API",
|
||||
"nav.developer_docs": "Документация для разработчиков",
|
||||
"nav.dark_mode": "Тёмная тема",
|
||||
"nav.light_mode": "Светлая тема",
|
||||
"nav.toggle_dark_mode": "Переключить тёмную тему",
|
||||
"nav.toggle_nav": "Переключить меню навигации",
|
||||
"nav.open_main_menu": "Открыть главное меню",
|
||||
"nav.skip_to_content": "Перейти к основному содержанию",
|
||||
"nav.main_navigation": "Основная навигация",
|
||||
"nav.admin_menu": "Меню администратора",
|
||||
"nav.admin_actions": "Действия администратора",
|
||||
"nav.help_center": "Центр помощи",
|
||||
|
||||
"auth.login": "Войти",
|
||||
"auth.logout": "Выйти",
|
||||
"auth.signup": "Регистрация",
|
||||
"auth.my_account": "Мой аккаунт",
|
||||
"auth.profile": "Профиль",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Конфиденциальность",
|
||||
"footer.imprint": "Выходные данные",
|
||||
"footer.terms": "Условия",
|
||||
"footer.cookies": "Файлы cookie",
|
||||
"footer.license": "Лицензия",
|
||||
"footer.attributions": "Атрибуции",
|
||||
"footer.version": "Версия {version}",
|
||||
"footer.navigation": "Навигация подвала",
|
||||
|
||||
"cookie.notice": "DocuElevate использует только необходимые сессионные файлы cookie для аутентификации и работы сервиса. Файлы cookie для отслеживания и аналитики не используются.",
|
||||
"cookie.policy_link": "Политика файлов cookie",
|
||||
"cookie.privacy_link": "Уведомление о конфиденциальности",
|
||||
"cookie.accept": "Понятно",
|
||||
"cookie.notice_label": "Уведомление о файлах cookie",
|
||||
|
||||
"common.save": "Сохранить",
|
||||
"common.cancel": "Отмена",
|
||||
"common.delete": "Удалить",
|
||||
"common.edit": "Редактировать",
|
||||
"common.close": "Закрыть",
|
||||
"common.confirm": "Подтвердить",
|
||||
"common.back": "Назад",
|
||||
"common.next": "Далее",
|
||||
"common.loading": "Загрузка...",
|
||||
"common.error": "Ошибка",
|
||||
"common.success": "Успешно",
|
||||
"common.warning": "Предупреждение",
|
||||
"common.info": "Информация",
|
||||
"common.yes": "Да",
|
||||
"common.no": "Нет",
|
||||
"common.search": "Поиск",
|
||||
"common.filter": "Фильтр",
|
||||
"common.reset": "Сбросить",
|
||||
"common.refresh": "Обновить",
|
||||
"common.download": "Скачать",
|
||||
"common.actions": "Действия",
|
||||
"common.details": "Подробности",
|
||||
"common.name": "Название",
|
||||
"common.description": "Описание",
|
||||
"common.type": "Тип",
|
||||
"common.status": "Статус",
|
||||
"common.date": "Дата",
|
||||
"common.size": "Размер",
|
||||
"common.created": "Создано",
|
||||
"common.updated": "Обновлено",
|
||||
"common.enabled": "Включено",
|
||||
"common.disabled": "Отключено",
|
||||
"common.active": "Активно",
|
||||
"common.inactive": "Неактивно",
|
||||
"common.all": "Все",
|
||||
"common.none": "Нет",
|
||||
"common.select": "Выбрать",
|
||||
"common.upload": "Загрузить",
|
||||
"common.processing": "Обработка",
|
||||
"common.completed": "Завершено",
|
||||
"common.failed": "Ошибка",
|
||||
"common.pending": "В ожидании",
|
||||
"common.retry": "Повторить",
|
||||
"common.view": "Просмотр",
|
||||
"common.copy": "Копировать",
|
||||
"common.copied": "Скопировано!",
|
||||
|
||||
"language.selector": "Язык",
|
||||
"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}",
|
||||
|
||||
"dashboard.title": "Панель управления",
|
||||
"dashboard.total_files": "Всего файлов",
|
||||
"dashboard.files_today": "Файлы сегодня",
|
||||
"dashboard.files_this_month": "Файлы за месяц",
|
||||
"dashboard.ocr_processed": "OCR обработано",
|
||||
"dashboard.active_integrations": "Активные интеграции",
|
||||
"dashboard.storage_targets": "Хранилища",
|
||||
"dashboard.recent_activity": "Последняя активность",
|
||||
"dashboard.quick_actions": "Быстрые действия",
|
||||
"dashboard.welcome": "Добро пожаловать в DocuElevate",
|
||||
|
||||
"upload.title": "Загрузить документ",
|
||||
"upload.drag_drop": "Перетащите файлы сюда или нажмите для выбора",
|
||||
"upload.select_file": "Выбрать файл",
|
||||
"upload.uploading": "Загрузка...",
|
||||
"upload.success": "Файл успешно загружен",
|
||||
"upload.error": "Ошибка загрузки",
|
||||
"upload.max_size": "Максимальный размер файла: {size}",
|
||||
|
||||
"files.title": "Файлы",
|
||||
"files.no_files": "Файлы не найдены",
|
||||
"files.filename": "Имя файла",
|
||||
"files.document_title": "Название документа",
|
||||
"files.uploaded": "Загружено",
|
||||
"files.file_size": "Размер файла",
|
||||
"files.ocr_status": "Статус OCR",
|
||||
"files.tags": "Теги",
|
||||
|
||||
"search.title": "Поиск документов",
|
||||
"search.placeholder": "Поиск по имени, содержимому, тегам...",
|
||||
"search.no_results": "Результаты не найдены",
|
||||
"search.results_count": "Найдено результатов: {count}",
|
||||
|
||||
"settings.title": "Настройки",
|
||||
"settings.save_success": "Настройка сохранена",
|
||||
"settings.save_error": "Не удалось сохранить настройку",
|
||||
"settings.reset_confirm": "Вы уверены, что хотите сбросить эту настройку?",
|
||||
|
||||
"integrations.title": "Интеграции",
|
||||
"integrations.connect": "Подключить",
|
||||
"integrations.disconnect": "Отключить",
|
||||
"integrations.connected": "Подключено",
|
||||
"integrations.not_connected": "Не подключено",
|
||||
"integrations.configure": "Настроить",
|
||||
|
||||
"pipelines.title": "Конвейеры обработки",
|
||||
"pipelines.create": "Создать конвейер",
|
||||
"pipelines.edit": "Редактировать конвейер",
|
||||
|
||||
"help.title": "Центр помощи",
|
||||
"help.getting_started": "Начало работы",
|
||||
"help.faq": "Часто задаваемые вопросы",
|
||||
"help.documentation": "Документация",
|
||||
"help.support": "Поддержка",
|
||||
|
||||
"error.not_found": "Страница не найдена",
|
||||
"error.not_found_message": "Запрашиваемая страница не существует.",
|
||||
"error.server_error": "Внутренняя ошибка сервера",
|
||||
"error.server_error_message": "Что-то пошло не так. Пожалуйста, попробуйте позже.",
|
||||
"error.unauthorized": "Не авторизован",
|
||||
"error.unauthorized_message": "Для доступа к этой странице необходимо войти в систему.",
|
||||
"error.forbidden": "Доступ запрещён",
|
||||
"error.forbidden_message": "У вас нет прав для доступа к этой странице.",
|
||||
|
||||
"notifications.title": "Уведомления",
|
||||
"notifications.mark_read": "Отметить как прочитанное",
|
||||
"notifications.mark_all_read": "Отметить все как прочитанные",
|
||||
"notifications.no_notifications": "Нет уведомлений",
|
||||
"notifications.unread_count": "{count} непрочитанных уведомлений"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "仪表盘",
|
||||
"nav.upload": "上传",
|
||||
"nav.files": "文件",
|
||||
"nav.search": "搜索",
|
||||
"nav.pipelines": "处理流程",
|
||||
"nav.integrations": "集成",
|
||||
"nav.help": "帮助",
|
||||
"nav.notifications": "通知",
|
||||
"nav.pricing": "价格",
|
||||
"nav.about": "关于",
|
||||
"nav.admin": "管理",
|
||||
"nav.settings": "设置",
|
||||
"nav.users": "用户",
|
||||
"nav.plan_designer": "方案设计",
|
||||
"nav.credentials": "凭据",
|
||||
"nav.file_manager": "文件管理器",
|
||||
"nav.duplicates": "重复文件",
|
||||
"nav.similarity": "相似度",
|
||||
"nav.queue_monitor": "队列监控",
|
||||
"nav.scheduled_jobs": "计划任务",
|
||||
"nav.backup_restore": "备份与恢复",
|
||||
"nav.status": "状态",
|
||||
"nav.api_docs": "API 文档",
|
||||
"nav.developer_docs": "开发者文档",
|
||||
"nav.dark_mode": "深色模式",
|
||||
"nav.light_mode": "浅色模式",
|
||||
"nav.toggle_dark_mode": "切换深色模式",
|
||||
"nav.toggle_nav": "切换导航菜单",
|
||||
"nav.open_main_menu": "打开主菜单",
|
||||
"nav.skip_to_content": "跳至主要内容",
|
||||
"nav.main_navigation": "主导航",
|
||||
"nav.admin_menu": "管理菜单",
|
||||
"nav.admin_actions": "管理操作",
|
||||
"nav.help_center": "帮助中心",
|
||||
|
||||
"auth.login": "登录",
|
||||
"auth.logout": "退出",
|
||||
"auth.signup": "注册",
|
||||
"auth.my_account": "我的账户",
|
||||
"auth.profile": "个人资料",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "隐私",
|
||||
"footer.imprint": "法律声明",
|
||||
"footer.terms": "条款",
|
||||
"footer.cookies": "Cookie",
|
||||
"footer.license": "许可",
|
||||
"footer.attributions": "致谢",
|
||||
"footer.version": "版本 {version}",
|
||||
"footer.navigation": "页脚导航",
|
||||
|
||||
"cookie.notice": "DocuElevate 仅使用身份验证和服务运行所需的基本会话 Cookie。不使用任何跟踪或分析 Cookie。",
|
||||
"cookie.policy_link": "Cookie 政策",
|
||||
"cookie.privacy_link": "隐私声明",
|
||||
"cookie.accept": "我知道了",
|
||||
"cookie.notice_label": "Cookie 通知",
|
||||
|
||||
"common.save": "保存",
|
||||
"common.cancel": "取消",
|
||||
"common.delete": "删除",
|
||||
"common.edit": "编辑",
|
||||
"common.close": "关闭",
|
||||
"common.confirm": "确认",
|
||||
"common.back": "返回",
|
||||
"common.next": "下一步",
|
||||
"common.loading": "加载中...",
|
||||
"common.error": "错误",
|
||||
"common.success": "成功",
|
||||
"common.warning": "警告",
|
||||
"common.info": "信息",
|
||||
"common.yes": "是",
|
||||
"common.no": "否",
|
||||
"common.search": "搜索",
|
||||
"common.filter": "筛选",
|
||||
"common.reset": "重置",
|
||||
"common.refresh": "刷新",
|
||||
"common.download": "下载",
|
||||
"common.actions": "操作",
|
||||
"common.details": "详情",
|
||||
"common.name": "名称",
|
||||
"common.description": "描述",
|
||||
"common.type": "类型",
|
||||
"common.status": "状态",
|
||||
"common.date": "日期",
|
||||
"common.size": "大小",
|
||||
"common.created": "创建时间",
|
||||
"common.updated": "更新时间",
|
||||
"common.enabled": "已启用",
|
||||
"common.disabled": "已禁用",
|
||||
"common.active": "活跃",
|
||||
"common.inactive": "不活跃",
|
||||
"common.all": "全部",
|
||||
"common.none": "无",
|
||||
"common.select": "选择",
|
||||
"common.upload": "上传",
|
||||
"common.processing": "处理中",
|
||||
"common.completed": "已完成",
|
||||
"common.failed": "失败",
|
||||
"common.pending": "待处理",
|
||||
"common.retry": "重试",
|
||||
"common.view": "查看",
|
||||
"common.copy": "复制",
|
||||
"common.copied": "已复制!",
|
||||
|
||||
"language.selector": "语言",
|
||||
"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}",
|
||||
|
||||
"dashboard.title": "仪表盘",
|
||||
"dashboard.total_files": "文件总数",
|
||||
"dashboard.files_today": "今日文件",
|
||||
"dashboard.files_this_month": "本月文件",
|
||||
"dashboard.ocr_processed": "OCR 已处理",
|
||||
"dashboard.active_integrations": "活跃集成",
|
||||
"dashboard.storage_targets": "存储目标",
|
||||
"dashboard.recent_activity": "最近活动",
|
||||
"dashboard.quick_actions": "快捷操作",
|
||||
"dashboard.welcome": "欢迎使用 DocuElevate",
|
||||
|
||||
"upload.title": "上传文档",
|
||||
"upload.drag_drop": "将文件拖放到此处或点击浏览",
|
||||
"upload.select_file": "选择文件",
|
||||
"upload.uploading": "上传中...",
|
||||
"upload.success": "文件上传成功",
|
||||
"upload.error": "上传失败",
|
||||
"upload.max_size": "最大文件大小:{size}",
|
||||
|
||||
"files.title": "文件",
|
||||
"files.no_files": "未找到文件",
|
||||
"files.filename": "文件名",
|
||||
"files.document_title": "文档标题",
|
||||
"files.uploaded": "已上传",
|
||||
"files.file_size": "文件大小",
|
||||
"files.ocr_status": "OCR 状态",
|
||||
"files.tags": "标签",
|
||||
|
||||
"search.title": "搜索文档",
|
||||
"search.placeholder": "按文件名、内容、标签搜索...",
|
||||
"search.no_results": "未找到结果",
|
||||
"search.results_count": "找到 {count} 个结果",
|
||||
|
||||
"settings.title": "设置",
|
||||
"settings.save_success": "设置保存成功",
|
||||
"settings.save_error": "设置保存失败",
|
||||
"settings.reset_confirm": "确定要重置此设置吗?",
|
||||
|
||||
"integrations.title": "集成",
|
||||
"integrations.connect": "连接",
|
||||
"integrations.disconnect": "断开",
|
||||
"integrations.connected": "已连接",
|
||||
"integrations.not_connected": "未连接",
|
||||
"integrations.configure": "配置",
|
||||
|
||||
"pipelines.title": "处理流程",
|
||||
"pipelines.create": "创建流程",
|
||||
"pipelines.edit": "编辑流程",
|
||||
|
||||
"help.title": "帮助中心",
|
||||
"help.getting_started": "入门指南",
|
||||
"help.faq": "常见问题",
|
||||
"help.documentation": "文档",
|
||||
"help.support": "支持",
|
||||
|
||||
"error.not_found": "页面未找到",
|
||||
"error.not_found_message": "您要查找的页面不存在。",
|
||||
"error.server_error": "服务器内部错误",
|
||||
"error.server_error_message": "出了点问题,请稍后再试。",
|
||||
"error.unauthorized": "未授权",
|
||||
"error.unauthorized_message": "您需要登录才能访问此页面。",
|
||||
"error.forbidden": "禁止访问",
|
||||
"error.forbidden_message": "您没有权限访问此页面。",
|
||||
|
||||
"notifications.title": "通知",
|
||||
"notifications.mark_read": "标记为已读",
|
||||
"notifications.mark_all_read": "全部标记为已读",
|
||||
"notifications.no_notifications": "没有通知",
|
||||
"notifications.unread_count": "{count} 条未读通知"
|
||||
}
|
||||
@@ -61,6 +61,7 @@ from app.main import app as fastapi_app # noqa: E402
|
||||
# Import models to register them with SQLAlchemy Base
|
||||
from app.models import ( # noqa: F401, E402
|
||||
ApiToken,
|
||||
AuditLog,
|
||||
DocumentMetadata,
|
||||
FileRecord,
|
||||
Pipeline,
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Tests for the comprehensive audit logging feature.
|
||||
|
||||
Covers the audit service (recording, querying, SIEM forwarding),
|
||||
the REST API endpoints, and the admin viewer page.
|
||||
"""
|
||||
|
||||
import json
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.database import Base
|
||||
from app.models import AuditLog
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def audit_db():
|
||||
"""Fresh in-memory database with all tables created."""
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAuditLogModel:
|
||||
"""Verify the AuditLog ORM model."""
|
||||
|
||||
def test_create_minimal_entry(self, audit_db):
|
||||
"""Minimal required fields can be persisted."""
|
||||
entry = AuditLog(user="alice", action="login")
|
||||
audit_db.add(entry)
|
||||
audit_db.commit()
|
||||
audit_db.refresh(entry)
|
||||
assert entry.id is not None
|
||||
assert entry.user == "alice"
|
||||
assert entry.action == "login"
|
||||
assert entry.severity == "info" # server default
|
||||
|
||||
def test_create_full_entry(self, audit_db):
|
||||
"""All columns persist correctly."""
|
||||
entry = AuditLog(
|
||||
user="bob",
|
||||
action="document.create",
|
||||
resource_type="document",
|
||||
resource_id="42",
|
||||
ip_address="10.0.0.1",
|
||||
details='{"filename": "invoice.pdf"}',
|
||||
severity="warning",
|
||||
)
|
||||
audit_db.add(entry)
|
||||
audit_db.commit()
|
||||
audit_db.refresh(entry)
|
||||
assert entry.resource_type == "document"
|
||||
assert entry.resource_id == "42"
|
||||
assert entry.ip_address == "10.0.0.1"
|
||||
assert json.loads(entry.details) == {"filename": "invoice.pdf"}
|
||||
assert entry.severity == "warning"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Service tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAuditService:
|
||||
"""Verify the audit_service helper functions."""
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_record_event(self, mock_settings, audit_db):
|
||||
"""record_event persists a row and returns the entry."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import record_event
|
||||
|
||||
entry = record_event(
|
||||
audit_db,
|
||||
action="settings.update",
|
||||
user="admin",
|
||||
resource_type="settings",
|
||||
resource_id="openai_model",
|
||||
details={"old": "gpt-4", "new": "gpt-4o"},
|
||||
)
|
||||
assert entry.id is not None
|
||||
assert entry.action == "settings.update"
|
||||
assert entry.user == "admin"
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_query_events_no_filter(self, mock_settings, audit_db):
|
||||
"""query_events returns all events when no filter is supplied."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import query_events, record_event
|
||||
|
||||
for i in range(5):
|
||||
record_event(audit_db, action=f"action_{i}", user="sys")
|
||||
results = query_events(audit_db)
|
||||
assert len(results) == 5
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_query_events_filter_action(self, mock_settings, audit_db):
|
||||
"""query_events filters by action."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import query_events, record_event
|
||||
|
||||
record_event(audit_db, action="login", user="alice")
|
||||
record_event(audit_db, action="logout", user="alice")
|
||||
results = query_events(audit_db, action="login")
|
||||
assert len(results) == 1
|
||||
assert results[0].action == "login"
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_query_events_filter_user(self, mock_settings, audit_db):
|
||||
"""query_events filters by user."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import query_events, record_event
|
||||
|
||||
record_event(audit_db, action="login", user="alice")
|
||||
record_event(audit_db, action="login", user="bob")
|
||||
results = query_events(audit_db, user="bob")
|
||||
assert len(results) == 1
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_query_events_filter_severity(self, mock_settings, audit_db):
|
||||
"""query_events filters by severity."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import query_events, record_event
|
||||
|
||||
record_event(audit_db, action="fail", user="sys", severity="error")
|
||||
record_event(audit_db, action="ok", user="sys", severity="info")
|
||||
results = query_events(audit_db, severity="error")
|
||||
assert len(results) == 1
|
||||
assert results[0].severity == "error"
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_count_events(self, mock_settings, audit_db):
|
||||
"""count_events returns the correct total."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import count_events, record_event
|
||||
|
||||
for _ in range(3):
|
||||
record_event(audit_db, action="ping", user="sys")
|
||||
assert count_events(audit_db) == 3
|
||||
assert count_events(audit_db, action="ping") == 3
|
||||
assert count_events(audit_db, action="pong") == 0
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_query_events_pagination(self, mock_settings, audit_db):
|
||||
"""query_events respects limit and offset."""
|
||||
mock_settings.audit_siem_enabled = False
|
||||
from app.utils.audit_service import query_events, record_event
|
||||
|
||||
for i in range(10):
|
||||
record_event(audit_db, action=f"a{i}", user="sys")
|
||||
page1 = query_events(audit_db, limit=3, offset=0)
|
||||
page2 = query_events(audit_db, limit=3, offset=3)
|
||||
assert len(page1) == 3
|
||||
assert len(page2) == 3
|
||||
assert page1[0].id != page2[0].id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SIEM forwarding tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSIEMForwarding:
|
||||
"""Verify SIEM transport helpers."""
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
def test_build_siem_payload(self, mock_settings):
|
||||
"""_build_siem_payload returns a dict with all expected keys."""
|
||||
from app.utils.audit_service import _build_siem_payload
|
||||
|
||||
entry = AuditLog(
|
||||
id=1,
|
||||
user="admin",
|
||||
action="login",
|
||||
resource_type="session",
|
||||
timestamp=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
severity="info",
|
||||
)
|
||||
payload = _build_siem_payload(entry)
|
||||
assert payload["user"] == "admin"
|
||||
assert payload["action"] == "login"
|
||||
assert payload["source"] == "docuelevate"
|
||||
assert "timestamp" in payload
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
@patch("app.utils.audit_service.socket")
|
||||
def test_send_syslog_udp(self, mock_socket_mod, mock_settings):
|
||||
"""_send_syslog sends a UDP datagram to the configured host."""
|
||||
mock_settings.audit_siem_syslog_protocol = "udp"
|
||||
mock_settings.audit_siem_syslog_host = "127.0.0.1"
|
||||
mock_settings.audit_siem_syslog_port = 5140
|
||||
|
||||
mock_sock = MagicMock()
|
||||
mock_socket_mod.AF_INET = socket.AF_INET
|
||||
mock_socket_mod.SOCK_DGRAM = socket.SOCK_DGRAM
|
||||
mock_socket_mod.gethostname.return_value = "test-host"
|
||||
mock_socket_mod.socket.return_value.__enter__ = MagicMock(return_value=mock_sock)
|
||||
mock_socket_mod.socket.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
from app.utils.audit_service import _send_syslog
|
||||
|
||||
_send_syslog({"user": "test", "action": "login", "severity": "info", "timestamp": "2026-01-01T00:00:00"})
|
||||
mock_sock.sendto.assert_called_once()
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
@patch("app.utils.audit_service.httpx")
|
||||
def test_send_http_generic(self, mock_httpx, mock_settings):
|
||||
"""_send_http POSTs JSON to a generic endpoint."""
|
||||
mock_settings.audit_siem_http_url = "https://siem.example.com/ingest"
|
||||
mock_settings.audit_siem_http_token = "my-token"
|
||||
mock_settings.audit_siem_http_custom_headers = ""
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_client.post.return_value = mock_resp
|
||||
mock_httpx.Client.return_value.__enter__ = MagicMock(return_value=mock_client)
|
||||
mock_httpx.Client.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
from app.utils.audit_service import _send_http
|
||||
|
||||
_send_http({"user": "test", "action": "login"})
|
||||
mock_client.post.assert_called_once()
|
||||
call_kwargs = mock_client.post.call_args
|
||||
assert call_kwargs.kwargs["headers"]["Authorization"] == "Bearer my-token"
|
||||
|
||||
@patch("app.utils.audit_service.settings")
|
||||
@patch("app.utils.audit_service.httpx")
|
||||
def test_send_http_splunk_hec(self, mock_httpx, mock_settings):
|
||||
"""_send_http wraps payload in Splunk HEC envelope when URL contains /services/collector."""
|
||||
mock_settings.audit_siem_http_url = "https://splunk:8088/services/collector/event"
|
||||
mock_settings.audit_siem_http_token = "hec-token"
|
||||
mock_settings.audit_siem_http_custom_headers = ""
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_client.post.return_value = mock_resp
|
||||
mock_httpx.Client.return_value.__enter__ = MagicMock(return_value=mock_client)
|
||||
mock_httpx.Client.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
from app.utils.audit_service import _send_http
|
||||
|
||||
_send_http({"user": "test", "action": "login"})
|
||||
call_kwargs = mock_client.post.call_args
|
||||
body = call_kwargs.kwargs["json"]
|
||||
assert "event" in body
|
||||
assert body["sourcetype"] == "docuelevate:audit"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API endpoint tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAuditLogAPI:
|
||||
"""Test /api/audit-logs REST endpoints."""
|
||||
|
||||
def test_list_audit_logs_empty(self, client):
|
||||
"""GET /api/audit-logs returns empty list when no events exist."""
|
||||
resp = client.get("/api/audit-logs")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["items"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_list_audit_logs_with_data(self, client, db_session):
|
||||
"""GET /api/audit-logs returns recorded events."""
|
||||
entry = AuditLog(user="tester", action="test.action", severity="info")
|
||||
db_session.add(entry)
|
||||
db_session.commit()
|
||||
|
||||
resp = client.get("/api/audit-logs")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["action"] == "test.action"
|
||||
|
||||
def test_list_audit_logs_filter_by_action(self, client, db_session):
|
||||
"""GET /api/audit-logs?action=x filters correctly."""
|
||||
db_session.add(AuditLog(user="a", action="login", severity="info"))
|
||||
db_session.add(AuditLog(user="a", action="logout", severity="info"))
|
||||
db_session.commit()
|
||||
|
||||
resp = client.get("/api/audit-logs?action=login")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
|
||||
def test_list_distinct_actions(self, client, db_session):
|
||||
"""GET /api/audit-logs/actions returns distinct action values."""
|
||||
db_session.add(AuditLog(user="a", action="login", severity="info"))
|
||||
db_session.add(AuditLog(user="b", action="login", severity="info"))
|
||||
db_session.add(AuditLog(user="a", action="logout", severity="info"))
|
||||
db_session.commit()
|
||||
|
||||
resp = client.get("/api/audit-logs/actions")
|
||||
assert resp.status_code == 200
|
||||
actions = resp.json()
|
||||
assert set(actions) == {"login", "logout"}
|
||||
|
||||
def test_list_distinct_users(self, client, db_session):
|
||||
"""GET /api/audit-logs/users returns distinct user values."""
|
||||
db_session.add(AuditLog(user="alice", action="x", severity="info"))
|
||||
db_session.add(AuditLog(user="bob", action="x", severity="info"))
|
||||
db_session.commit()
|
||||
|
||||
resp = client.get("/api/audit-logs/users")
|
||||
assert resp.status_code == 200
|
||||
users = resp.json()
|
||||
assert set(users) == {"alice", "bob"}
|
||||
|
||||
def test_list_audit_logs_pagination(self, client, db_session):
|
||||
"""GET /api/audit-logs supports limit/offset pagination."""
|
||||
for i in range(5):
|
||||
db_session.add(AuditLog(user="u", action=f"a{i}", severity="info"))
|
||||
db_session.commit()
|
||||
|
||||
resp = client.get("/api/audit-logs?limit=2&offset=0")
|
||||
data = resp.json()
|
||||
assert len(data["items"]) == 2
|
||||
assert data["total"] == 5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# View tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAuditLogView:
|
||||
"""Test the admin audit-log viewer page."""
|
||||
|
||||
def test_audit_logs_page_loads(self, client):
|
||||
"""GET /admin/audit-logs returns 200 and renders the template."""
|
||||
resp = client.get("/admin/audit-logs")
|
||||
assert resp.status_code == 200
|
||||
assert "Audit Logs" in resp.text
|
||||
@@ -124,6 +124,85 @@ class TestInitDb:
|
||||
|
||||
test_engine.dispose()
|
||||
|
||||
def test_init_db_creates_shared_links_for_database_missing_table(self, tmp_path):
|
||||
"""Regression test: databases at revision 026 that skipped 025_add_shared_links.
|
||||
|
||||
Migration 025_add_shared_links was inserted into the chain between
|
||||
024_add_api_tokens and 025_add_user_notifications after some databases
|
||||
had already been migrated past that point. Migration 027 creates the
|
||||
table idempotently so those databases are repaired.
|
||||
"""
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
|
||||
db_path = str(tmp_path / "regression_shared_links.db")
|
||||
test_engine = create_engine(f"sqlite:///{db_path}")
|
||||
|
||||
# Set up a database at revision 026 but WITHOUT the shared_links table.
|
||||
# This simulates a DB that was migrated before 025_add_shared_links
|
||||
# was inserted into the chain.
|
||||
with test_engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE files ("
|
||||
"id INTEGER PRIMARY KEY, filehash VARCHAR NOT NULL, "
|
||||
"original_filename VARCHAR, local_filename VARCHAR NOT NULL, "
|
||||
"original_file_path VARCHAR, processed_file_path VARCHAR, "
|
||||
"file_size INTEGER NOT NULL, mime_type VARCHAR, "
|
||||
"is_duplicate BOOLEAN DEFAULT 0 NOT NULL, duplicate_of_id INTEGER, "
|
||||
"ocr_text TEXT, ai_metadata TEXT, document_title VARCHAR, "
|
||||
"ocr_quality_score INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE processing_logs ("
|
||||
"id INTEGER PRIMARY KEY, file_id INTEGER, task_id VARCHAR, "
|
||||
"step_name VARCHAR, status VARCHAR, message VARCHAR, detail TEXT, "
|
||||
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE file_processing_steps ("
|
||||
"id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL, "
|
||||
"step_name VARCHAR NOT NULL, status VARCHAR NOT NULL, "
|
||||
"started_at DATETIME, completed_at DATETIME, error_message TEXT, "
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
||||
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE saved_searches ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, user_id VARCHAR NOT NULL, "
|
||||
"name VARCHAR NOT NULL, filters TEXT NOT NULL, "
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
||||
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
||||
"UNIQUE (user_id, name))"
|
||||
)
|
||||
)
|
||||
conn.execute(text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)"))
|
||||
conn.execute(text("INSERT INTO alembic_version VALUES ('026_add_scheduled_jobs')"))
|
||||
|
||||
with patch("app.database.engine", test_engine), patch("app.database.DB_URL", f"sqlite:///{db_path}"):
|
||||
init_db()
|
||||
|
||||
inspector = sa_inspect(test_engine)
|
||||
table_names = inspector.get_table_names()
|
||||
assert "shared_links" in table_names
|
||||
|
||||
# Verify the shared_links table has the expected columns.
|
||||
columns = {col["name"] for col in inspector.get_columns("shared_links")}
|
||||
assert "id" in columns
|
||||
assert "token" in columns
|
||||
assert "file_id" in columns
|
||||
assert "owner_id" in columns
|
||||
assert "expires_at" in columns
|
||||
assert "is_active" in columns
|
||||
|
||||
test_engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetDb:
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
"""Tests for the i18n (internationalization) and l10n (localization) utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
_parse_accept_language,
|
||||
detect_language,
|
||||
format_date,
|
||||
format_datetime,
|
||||
format_number,
|
||||
get_language_info,
|
||||
reload_translations,
|
||||
translate,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation file integrity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTranslationFiles:
|
||||
"""Verify that all translation JSON files are valid and complete."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache(self) -> None:
|
||||
"""Clear translation cache before each test."""
|
||||
reload_translations()
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_translation_files_exist(self) -> None:
|
||||
"""Every supported language must have a corresponding JSON file."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
assert filepath.is_file(), f"Missing translation file for {lang['code']}"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_translation_files_are_valid_json(self) -> None:
|
||||
"""All translation files must be parseable JSON."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
data = json.loads(filepath.read_text(encoding="utf-8"))
|
||||
assert isinstance(data, dict), f"{lang['code']}.json must be a dict"
|
||||
assert len(data) > 0, f"{lang['code']}.json must not be empty"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_languages_have_same_keys(self) -> None:
|
||||
"""All translation files should have the same set of keys as English."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
en_path = translations_dir / "en.json"
|
||||
en_keys = set(json.loads(en_path.read_text(encoding="utf-8")).keys())
|
||||
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if lang["code"] == "en":
|
||||
continue
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
lang_keys = set(json.loads(filepath.read_text(encoding="utf-8")).keys())
|
||||
missing = en_keys - lang_keys
|
||||
assert not missing, f"{lang['code']}.json missing keys: {missing}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core translate() function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTranslate:
|
||||
"""Tests for the translate() function."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache(self) -> None:
|
||||
reload_translations()
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_english_key(self) -> None:
|
||||
"""English keys should resolve to English text."""
|
||||
result = translate("nav.dashboard", "en")
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_german_key(self) -> None:
|
||||
"""German locale should return German text."""
|
||||
result = translate("nav.dashboard", "de")
|
||||
assert result == "Übersicht"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_french_key(self) -> None:
|
||||
"""French locale should return French text."""
|
||||
result = translate("nav.dashboard", "fr")
|
||||
assert result == "Tableau de bord"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_chinese_key(self) -> None:
|
||||
"""Chinese locale should return Chinese text."""
|
||||
result = translate("nav.dashboard", "zh")
|
||||
assert result == "仪表盘"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_fallback_to_english(self) -> None:
|
||||
"""Unknown locale falls back to English."""
|
||||
result = translate("nav.dashboard", "xx")
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_missing_key_returns_key(self) -> None:
|
||||
"""Missing key falls back to the key itself."""
|
||||
result = translate("nonexistent.key", "en")
|
||||
assert result == "nonexistent.key"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_none_locale_uses_default(self) -> None:
|
||||
"""None locale defaults to English."""
|
||||
result = translate("nav.dashboard", None)
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_with_kwargs(self) -> None:
|
||||
"""Placeholders should be interpolated via kwargs."""
|
||||
result = translate("footer.copyright", "en", year="2025")
|
||||
assert result == "DocuElevate 2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_with_kwargs_german(self) -> None:
|
||||
"""Placeholder interpolation in German."""
|
||||
result = translate("language.changed", "de", language="English")
|
||||
assert result == "Sprache geändert zu English"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Accept-Language header parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseAcceptLanguage:
|
||||
"""Tests for parsing the Accept-Language HTTP header."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_simple_language(self) -> None:
|
||||
assert _parse_accept_language("de") == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_language_with_region(self) -> None:
|
||||
assert _parse_accept_language("de-DE") == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_multiple_languages_quality(self) -> None:
|
||||
result = _parse_accept_language("fr;q=0.9, de;q=1.0, en;q=0.8")
|
||||
assert result == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unsupported_language_fallback(self) -> None:
|
||||
result = _parse_accept_language("ja, ko")
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_header(self) -> None:
|
||||
assert _parse_accept_language("") is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_complex_accept_language(self) -> None:
|
||||
header = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
|
||||
result = _parse_accept_language(header)
|
||||
assert result == "zh"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDetectLanguage:
|
||||
"""Tests for detecting language from request context."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_session_preference_takes_priority(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {"preferred_language": "de"}
|
||||
request.cookies = {}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_cookie_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {"docuelevate_lang": "fr"}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "fr"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_accept_language_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {}
|
||||
request.headers = {"accept-language": "es-ES,es;q=0.9"}
|
||||
assert detect_language(request) == "es"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == DEFAULT_LANGUAGE
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_invalid_session_language_ignored(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {"preferred_language": "invalid"}
|
||||
request.cookies = {"docuelevate_lang": "it"}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "it"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Localization helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestL10nFormatters:
|
||||
"""Tests for locale-aware formatting functions."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_english(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "en")
|
||||
assert "March" in result
|
||||
assert "15" in result
|
||||
assert "2025" in result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_german(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "de")
|
||||
assert "15." in result
|
||||
assert "2025" in result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_short(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "en", short=True)
|
||||
assert result == "03/15/2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_short_german(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "de", short=True)
|
||||
assert result == "15.03.2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_none(self) -> None:
|
||||
assert format_date(None) == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_datetime_none(self) -> None:
|
||||
assert format_datetime(None) == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_english(self) -> None:
|
||||
result = format_number(1234567, "en")
|
||||
assert result == "1,234,567"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_german(self) -> None:
|
||||
result = format_number(1234567, "de")
|
||||
assert result == "1.234.567"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_float_english(self) -> None:
|
||||
result = format_number(1234.56, "en")
|
||||
assert result == "1,234.56"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_float_german(self) -> None:
|
||||
result = format_number(1234.56, "de")
|
||||
assert result == "1.234,56"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_datetime_chinese(self) -> None:
|
||||
dt = datetime(2025, 3, 15, 14, 30)
|
||||
result = format_datetime(dt, "zh")
|
||||
assert "2025" in result
|
||||
assert "03" in result
|
||||
assert "15" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_language_info()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetLanguageInfo:
|
||||
"""Tests for get_language_info() utility."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_known_language(self) -> None:
|
||||
info = get_language_info("de")
|
||||
assert info is not None
|
||||
assert info["name"] == "German"
|
||||
assert info["native"] == "Deutsch"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_language(self) -> None:
|
||||
assert get_language_info("xx") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SUPPORTED_LANGUAGES metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSupportedLanguages:
|
||||
"""Tests for language metadata constants."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ten_languages_supported(self) -> None:
|
||||
assert len(SUPPORTED_LANGUAGES) == 10
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_supported_codes_set(self) -> None:
|
||||
expected = {"en", "de", "fr", "es", "it", "pt", "nl", "pl", "zh", "ru"}
|
||||
assert SUPPORTED_LANGUAGE_CODES == expected
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_language_is_english(self) -> None:
|
||||
assert DEFAULT_LANGUAGE == "en"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API endpoint tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestI18nAPI:
|
||||
"""Tests for the i18n API endpoints."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_list_languages(self, client: TestClient) -> None:
|
||||
"""GET /api/i18n/languages should return all supported languages."""
|
||||
response = client.get("/api/i18n/languages")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "languages" in data
|
||||
assert len(data["languages"]) == 10
|
||||
assert data["default"] == "en"
|
||||
# Verify each language has required fields
|
||||
for lang in data["languages"]:
|
||||
assert "code" in lang
|
||||
assert "name" in lang
|
||||
assert "native" in lang
|
||||
assert "flag" in lang
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language(self, client: TestClient) -> None:
|
||||
"""POST /api/i18n/language should set language preference."""
|
||||
response = client.post(
|
||||
"/api/i18n/language",
|
||||
json={"language": "de"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["language"] == "de"
|
||||
# Verify cookie was set
|
||||
assert "docuelevate_lang" in response.cookies
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language_invalid_falls_back_to_default(self, client: TestClient) -> None:
|
||||
"""Invalid language code should fall back to default."""
|
||||
response = client.post(
|
||||
"/api/i18n/language",
|
||||
json={"language": "invalid"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["language"] == "en"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language_persists_in_cookie(self, client: TestClient) -> None:
|
||||
"""Language setting should be persisted in a cookie."""
|
||||
client.post("/api/i18n/language", json={"language": "fr"})
|
||||
# Subsequent requests should detect the language from cookie
|
||||
response = client.get("/api/i18n/languages")
|
||||
data = response.json()
|
||||
assert data["current"] == "fr"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_base_html_uses_current_locale(self, client: TestClient) -> None:
|
||||
"""The base template should set lang attribute to current locale."""
|
||||
# Set language to German
|
||||
client.post("/api/i18n/language", json={"language": "de"})
|
||||
# Load homepage
|
||||
response = client.get("/", follow_redirects=True)
|
||||
assert response.status_code == 200
|
||||
# The lang attribute should reflect the locale
|
||||
assert 'lang="de"' in response.text or 'lang="en"' in response.text
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_language_selector_in_nav(self, client: TestClient) -> None:
|
||||
"""The navigation should contain the language selector globe icon."""
|
||||
response = client.get("/", follow_redirects=True)
|
||||
if response.status_code == 200:
|
||||
assert "fa-globe" in response.text
|
||||
assert "setLanguage" in response.text
|
||||
@@ -0,0 +1,608 @@
|
||||
"""Tests to boost code coverage for all view modules below 100%.
|
||||
|
||||
Covers: api_tokens, notifications, shared_links, share, plans,
|
||||
imap_accounts, integrations, general, filemanager, files, help.
|
||||
"""
|
||||
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.config import settings as app_settings
|
||||
from app.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def _fresh_db():
|
||||
"""Yield a fresh in-memory SQLite session."""
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = sessionmaker(autocommit=False, autoflush=False, bind=engine)()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client_fresh(_fresh_db) -> TestClient:
|
||||
"""TestClient backed by a fresh database."""
|
||||
|
||||
def _override():
|
||||
try:
|
||||
yield _fresh_db
|
||||
finally:
|
||||
pass
|
||||
|
||||
app.dependency_overrides[get_db] = _override
|
||||
with TestClient(app, base_url="http://localhost") as tc:
|
||||
yield tc
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 1. Simple template-render views (api_tokens, notifications,
|
||||
# shared_links, share, plans)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestApiTokensView:
|
||||
"""GET /api-tokens should render the management page."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_api_tokens_page_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/api-tokens")
|
||||
assert resp.status_code == 200
|
||||
assert "API Tokens" in resp.text
|
||||
|
||||
|
||||
class TestNotificationsView:
|
||||
"""GET /notifications should render the dashboard."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_notifications_page_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/notifications")
|
||||
assert resp.status_code == 200
|
||||
assert "Notifications" in resp.text
|
||||
|
||||
|
||||
class TestSharedLinksView:
|
||||
"""GET /shared-links should render the management page."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_shared_links_page_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/shared-links")
|
||||
assert resp.status_code == 200
|
||||
assert "Shared Links" in resp.text
|
||||
|
||||
|
||||
class TestShareView:
|
||||
"""GET /share/{token} should render the public share landing page."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_share_page_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/share/abc123")
|
||||
assert resp.status_code == 200
|
||||
# Token should be passed to the template
|
||||
assert "abc123" in resp.text
|
||||
|
||||
|
||||
class TestPlansViews:
|
||||
"""GET /admin/plans and /admin/stripe-wizard should render pages."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_plan_designer_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/admin/plans")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_stripe_wizard_returns_200(self, client_fresh: TestClient):
|
||||
resp = client_fresh.get("/admin/stripe-wizard")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 2. imap_accounts view (30.95 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestImapAccountsView:
|
||||
"""Tests for /imap-accounts view."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_imap_accounts_page_no_owner(self, client_fresh: TestClient):
|
||||
"""When no owner_id is resolved, page renders with defaults."""
|
||||
resp = client_fresh.get("/imap-accounts")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_imap_accounts_page_with_owner(self, _fresh_db, client_fresh: TestClient):
|
||||
"""When session has a user, the page queries IMAP accounts."""
|
||||
with patch("app.views.imap_accounts.get_current_owner_id", return_value="testuser"):
|
||||
with patch(
|
||||
"app.views.imap_accounts.get_user_tier_id",
|
||||
return_value="starter",
|
||||
):
|
||||
with patch(
|
||||
"app.views.imap_accounts.get_tier",
|
||||
return_value={"id": "starter", "name": "Starter", "max_mailboxes": 3},
|
||||
):
|
||||
resp = client_fresh.get("/imap-accounts")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_mailboxes_free_tier(self):
|
||||
"""Free tier should return 0 mailboxes."""
|
||||
from app.views.imap_accounts import _get_max_mailboxes
|
||||
|
||||
assert _get_max_mailboxes({"id": "free", "max_mailboxes": 0}) == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_mailboxes_unlimited(self):
|
||||
"""When max_mailboxes is 0 on a non-free tier, it means unlimited."""
|
||||
from app.views.imap_accounts import _get_max_mailboxes
|
||||
|
||||
assert _get_max_mailboxes({"id": "power", "max_mailboxes": 0}) is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_mailboxes_limited(self):
|
||||
"""When max_mailboxes > 0, return that value."""
|
||||
from app.views.imap_accounts import _get_max_mailboxes
|
||||
|
||||
assert _get_max_mailboxes({"id": "starter", "max_mailboxes": 5}) == 5
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 3. integrations view (82.09 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestIntegrationsView:
|
||||
"""Tests for /integrations view."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_integrations_dashboard_no_owner(self, client_fresh: TestClient):
|
||||
"""When no owner, the dashboard renders with zero-count defaults."""
|
||||
resp = client_fresh.get("/integrations")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_integrations_dashboard_with_owner(self, _fresh_db, client_fresh: TestClient):
|
||||
"""When an owner_id is resolved, DB queries run and tier is fetched."""
|
||||
with patch("app.views.integrations.get_current_owner_id", return_value="testuser"):
|
||||
with patch("app.views.integrations.get_user_tier_id", return_value="power"):
|
||||
with patch(
|
||||
"app.views.integrations.get_tier",
|
||||
return_value={
|
||||
"id": "power",
|
||||
"name": "Power",
|
||||
"max_storage_destinations": 0,
|
||||
"max_mailboxes": 0,
|
||||
},
|
||||
):
|
||||
resp = client_fresh.get("/integrations")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_integrations_dashboard_generic_exception(self, client_fresh: TestClient):
|
||||
"""A non-HTTP exception in the dashboard returns 500."""
|
||||
with patch(
|
||||
"app.views.integrations.get_current_owner_id",
|
||||
side_effect=RuntimeError("boom"),
|
||||
):
|
||||
resp = client_fresh.get("/integrations")
|
||||
assert resp.status_code == 500
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_integrations_dashboard_http_exception_passthrough(self, client_fresh: TestClient):
|
||||
"""An HTTPException inside the dashboard is re-raised, not wrapped in 500."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch(
|
||||
"app.views.integrations.get_current_owner_id",
|
||||
side_effect=HTTPException(status_code=403, detail="Forbidden"),
|
||||
):
|
||||
resp = client_fresh.get("/integrations")
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_destinations_free_default(self):
|
||||
from app.views.integrations import _get_max_destinations
|
||||
|
||||
assert _get_max_destinations({"id": "free", "max_storage_destinations": 0}) == 1
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_destinations_free_with_value(self):
|
||||
from app.views.integrations import _get_max_destinations
|
||||
|
||||
assert _get_max_destinations({"id": "free", "max_storage_destinations": 3}) == 3
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_destinations_unlimited(self):
|
||||
from app.views.integrations import _get_max_destinations
|
||||
|
||||
assert _get_max_destinations({"id": "power", "max_storage_destinations": 0}) is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_destinations_limited(self):
|
||||
from app.views.integrations import _get_max_destinations
|
||||
|
||||
assert _get_max_destinations({"id": "starter", "max_storage_destinations": 5}) == 5
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_sources_free(self):
|
||||
from app.views.integrations import _get_max_sources
|
||||
|
||||
assert _get_max_sources({"id": "free", "max_mailboxes": 0}) == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_sources_unlimited(self):
|
||||
from app.views.integrations import _get_max_sources
|
||||
|
||||
assert _get_max_sources({"id": "power", "max_mailboxes": 0}) is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_max_sources_limited(self):
|
||||
from app.views.integrations import _get_max_sources
|
||||
|
||||
assert _get_max_sources({"id": "starter", "max_mailboxes": 2}) == 2
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 4. general view (88.68 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestGeneralViewMultiUser:
|
||||
"""Cover the multi_user_enabled subscription branch (lines 96-105)."""
|
||||
|
||||
@staticmethod
|
||||
def _signed_session(user_data: dict) -> str:
|
||||
"""Create a signed Starlette session cookie containing user_data."""
|
||||
import json
|
||||
from base64 import b64encode
|
||||
|
||||
from itsdangerous import TimestampSigner
|
||||
|
||||
secret = os.environ.get(
|
||||
"SESSION_SECRET",
|
||||
"test_secret_key_for_testing_must_be_at_least_32_characters_long",
|
||||
)
|
||||
signer = TimestampSigner(secret)
|
||||
data = {"user": user_data}
|
||||
return signer.sign(b64encode(json.dumps(data).encode("utf-8"))).decode("utf-8")
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_home_page_multi_user_with_subscription(self, _fresh_db, client_fresh: TestClient):
|
||||
"""When multi_user_enabled is True and user has owner_id, subscription info is fetched.
|
||||
|
||||
Lines 96-103: Exercises the subscription lookup path.
|
||||
"""
|
||||
cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
|
||||
tier_mock = {
|
||||
"id": "starter",
|
||||
"name": "Starter",
|
||||
"lifetime_file_limit": 1000,
|
||||
"daily_upload_limit": 50,
|
||||
"monthly_upload_limit": 500,
|
||||
}
|
||||
usage_mock = {"lifetime": 10, "today": 2, "month": 8}
|
||||
with (
|
||||
patch.object(app_settings, "multi_user_enabled", True),
|
||||
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
||||
patch("app.views.general.get_provider_status", return_value={}),
|
||||
patch("app.views.general.validate_storage_configs", return_value={}),
|
||||
patch("app.utils.subscription.get_user_tier_id", return_value="starter"),
|
||||
patch("app.utils.subscription.get_tier", return_value=tier_mock),
|
||||
patch("app.utils.subscription.get_user_usage", return_value=usage_mock),
|
||||
):
|
||||
client_fresh.cookies.set("session", cookie_val)
|
||||
resp = client_fresh.get("/?setup=complete")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_home_page_multi_user_subscription_error(self, _fresh_db, client_fresh: TestClient):
|
||||
"""When subscription lookup fails, error is logged but page still renders.
|
||||
|
||||
Lines 104-105: Exercises the exception handling branch.
|
||||
"""
|
||||
cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
|
||||
with (
|
||||
patch.object(app_settings, "multi_user_enabled", True),
|
||||
patch("app.utils.setup_wizard.is_setup_required", return_value=False),
|
||||
patch("app.views.general.get_provider_status", return_value={}),
|
||||
patch("app.views.general.validate_storage_configs", return_value={}),
|
||||
patch("app.utils.subscription.get_user_tier_id", side_effect=RuntimeError("boom")),
|
||||
):
|
||||
client_fresh.cookies.set("session", cookie_val)
|
||||
resp = client_fresh.get("/?setup=complete")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 5. filemanager view (96.63 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestFilemanagerCoverageGaps:
|
||||
"""Cover the remaining gaps in filemanager.py."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_size_petabytes(self):
|
||||
"""Line 43: _format_size should return PB for very large sizes."""
|
||||
from app.views.filemanager import _format_size
|
||||
|
||||
# 1 PB = 1024^5 bytes
|
||||
one_pb = 1024**5
|
||||
result = _format_size(one_pb)
|
||||
assert "PB" in result
|
||||
assert "1.0 PB" == result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_size_multiple_petabytes(self):
|
||||
"""Large values above 1 PB."""
|
||||
from app.views.filemanager import _format_size
|
||||
|
||||
result = _format_size(5 * 1024**5)
|
||||
assert "PB" in result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_scan_dir_with_broken_symlink(self, tmp_path):
|
||||
"""Lines 104-106: files that cannot be stat'd are skipped with a warning.
|
||||
|
||||
Using a broken symlink to trigger OSError on stat().
|
||||
"""
|
||||
from app.views.filemanager import _scan_dir
|
||||
|
||||
# Create a broken symlink — stat() will raise FileNotFoundError (subclass of OSError)
|
||||
broken_link = tmp_path / "broken_link.txt"
|
||||
broken_link.symlink_to("/nonexistent/target/file")
|
||||
|
||||
# Also create a valid file so we can verify it's included
|
||||
valid_file = tmp_path / "valid.txt"
|
||||
valid_file.write_text("hello")
|
||||
|
||||
db_paths: set[str] = set()
|
||||
entries = _scan_dir(tmp_path, tmp_path, db_paths)
|
||||
|
||||
# The broken symlink should be skipped, the valid file should be included
|
||||
entry_names = [e["name"] for e in entries]
|
||||
assert "broken_link.txt" not in entry_names
|
||||
assert "valid.txt" in entry_names
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_scan_dir_oserror(self, tmp_path):
|
||||
"""OSError during stat in _scan_dir is caught and file is skipped.
|
||||
|
||||
We create a second broken symlink for this test.
|
||||
"""
|
||||
from app.views.filemanager import _scan_dir
|
||||
|
||||
broken_link = tmp_path / "also_broken.txt"
|
||||
broken_link.symlink_to("/another/nonexistent/path")
|
||||
|
||||
valid_file = tmp_path / "good.txt"
|
||||
valid_file.write_text("ok")
|
||||
db_paths: set[str] = set()
|
||||
|
||||
entries = _scan_dir(tmp_path, tmp_path, db_paths)
|
||||
entry_names = [e["name"] for e in entries]
|
||||
assert "also_broken.txt" not in entry_names
|
||||
assert "good.txt" in entry_names
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_walk_all_files_with_broken_symlink(self, tmp_path):
|
||||
"""Lines 146-147: files that fail stat during walk are skipped.
|
||||
|
||||
Using a broken symlink to trigger OSError.
|
||||
"""
|
||||
from app.views.filemanager import _walk_all_files
|
||||
|
||||
broken_link = tmp_path / "broken.pdf"
|
||||
broken_link.symlink_to("/nonexistent/target/file")
|
||||
|
||||
valid_file = tmp_path / "valid.pdf"
|
||||
valid_file.write_text("content")
|
||||
|
||||
db_paths: set[str] = set()
|
||||
entries = _walk_all_files(tmp_path, db_paths)
|
||||
|
||||
entry_names = [e["name"] for e in entries]
|
||||
assert "broken.pdf" not in entry_names
|
||||
assert "valid.pdf" in entry_names
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 6. files view (99.21 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestFilesViewCoverageGaps:
|
||||
"""Cover the remaining branches in files.py."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_compute_processing_flow_with_pipeline_steps(self):
|
||||
"""Lines 504-515: pipeline_steps filtering in _compute_processing_flow."""
|
||||
from app.views.files import _compute_processing_flow
|
||||
|
||||
# Create mock pipeline steps
|
||||
ps1 = SimpleNamespace(enabled=True, step_type="ocr")
|
||||
ps2 = SimpleNamespace(enabled=False, step_type="extract_metadata")
|
||||
ps3 = SimpleNamespace(enabled=True, step_type="send_to_destinations")
|
||||
|
||||
# Create mock logs with all required attributes including task_id
|
||||
log1 = SimpleNamespace(
|
||||
step_name="create_file_record",
|
||||
status="completed",
|
||||
message="ok",
|
||||
timestamp=None,
|
||||
started_at=None,
|
||||
completed_at=None,
|
||||
task_id="task-001",
|
||||
)
|
||||
log2 = SimpleNamespace(
|
||||
step_name="check_text",
|
||||
status="completed",
|
||||
message="ok",
|
||||
timestamp=None,
|
||||
started_at=None,
|
||||
completed_at=None,
|
||||
task_id="task-002",
|
||||
)
|
||||
|
||||
result = _compute_processing_flow([log1, log2], pipeline_steps=[ps1, ps2, ps3])
|
||||
|
||||
# _compute_processing_flow returns a list of stage dicts
|
||||
stage_keys = [s["key"] for s in result]
|
||||
assert "create_file_record" in stage_keys # always shown
|
||||
assert "check_text" in stage_keys # OCR step type + ran
|
||||
# extract_metadata is disabled, so its stages should NOT be included
|
||||
assert "extract_metadata_with_gpt" not in stage_keys
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_compute_processing_flow_with_pipeline_steps_none(self):
|
||||
"""When pipeline_steps is None, all stages are shown."""
|
||||
from app.views.files import _compute_processing_flow
|
||||
|
||||
result = _compute_processing_flow([], pipeline_steps=None)
|
||||
stage_keys = [s["key"] for s in result]
|
||||
assert "create_file_record" in stage_keys
|
||||
assert "extract_metadata_with_gpt" in stage_keys
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_compute_processing_flow_with_empty_pipeline_steps(self):
|
||||
"""When pipeline_steps is empty list, only always-show + ran stages remain."""
|
||||
from app.views.files import _compute_processing_flow
|
||||
|
||||
result = _compute_processing_flow([], pipeline_steps=[])
|
||||
stage_keys = [s["key"] for s in result]
|
||||
assert "create_file_record" in stage_keys
|
||||
# Other stages should be filtered out
|
||||
assert "convert_to_pdf" not in stage_keys
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_compute_processing_flow_dedup_enabled(self):
|
||||
"""When dedup is enabled and shown, check_for_duplicates stage appears."""
|
||||
from app.views.files import _compute_processing_flow
|
||||
|
||||
with (
|
||||
patch.object(app_settings, "enable_deduplication", True),
|
||||
patch.object(app_settings, "show_deduplication_step", True),
|
||||
):
|
||||
result = _compute_processing_flow([], pipeline_steps=None)
|
||||
stage_keys = [s["key"] for s in result]
|
||||
assert "check_for_duplicates" in stage_keys
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_file_detail_safe_exists_value_error(self, _fresh_db, client_fresh: TestClient):
|
||||
"""Test that _safe_exists handles ValueError from commonpath gracefully.
|
||||
|
||||
Lines 240-241: When os.path.commonpath raises ValueError (e.g., paths
|
||||
on different drives on Windows), _safe_exists returns False.
|
||||
"""
|
||||
from app.models import FileRecord
|
||||
|
||||
# Create a file record with all required fields
|
||||
rec = FileRecord(
|
||||
original_filename="test.pdf",
|
||||
local_filename="/tmp/test_local.pdf",
|
||||
original_file_path="/tmp/test_original.pdf",
|
||||
processed_file_path="/tmp/test_processed.pdf",
|
||||
file_size=100,
|
||||
mime_type="application/pdf",
|
||||
filehash="abc123def456",
|
||||
)
|
||||
_fresh_db.add(rec)
|
||||
_fresh_db.commit()
|
||||
_fresh_db.refresh(rec)
|
||||
|
||||
# Patch commonpath to raise ValueError
|
||||
with patch("os.path.commonpath", side_effect=ValueError("different drives")):
|
||||
resp = client_fresh.get(f"/files/{rec.id}")
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 7. help view (96 % → 100 %)
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestHelpViewCoverageGaps:
|
||||
"""Cover the missing branch in help.py (34->37)."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_page_no_session_attr(self, client_fresh: TestClient):
|
||||
"""When no session user is set, defaults are used for Zammad widgets."""
|
||||
resp = client_fresh.get("/help")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_page_request_without_session(self):
|
||||
"""Direct function call where request has no session attribute.
|
||||
|
||||
Branch 34->37: when hasattr(request, 'session') is False.
|
||||
"""
|
||||
from app.views.help import help_center
|
||||
|
||||
# Create a mock request without session attribute
|
||||
mock_request = MagicMock(spec=[]) # spec=[] means no attributes
|
||||
# help_center checks hasattr(request, "session")
|
||||
# With spec=[], hasattr will return False
|
||||
|
||||
with patch("app.views.help.templates") as mock_templates:
|
||||
mock_templates.TemplateResponse.return_value = "ok"
|
||||
await help_center(mock_request)
|
||||
|
||||
# Template should be called with empty user context
|
||||
call_args = mock_templates.TemplateResponse.call_args
|
||||
ctx = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("context", {})
|
||||
assert ctx["user_name"] == ""
|
||||
assert ctx["user_email"] == ""
|
||||
assert ctx["user_id"] == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_page_with_logged_in_user(self, client_fresh: TestClient):
|
||||
"""When session has user data, Zammad widget fields are populated.
|
||||
|
||||
Covers lines 41-43 (user_name, user_email, user_id extraction).
|
||||
"""
|
||||
import json
|
||||
from base64 import b64encode
|
||||
|
||||
from itsdangerous import TimestampSigner
|
||||
|
||||
secret = os.environ.get(
|
||||
"SESSION_SECRET",
|
||||
"test_secret_key_for_testing_must_be_at_least_32_characters_long",
|
||||
)
|
||||
signer = TimestampSigner(secret)
|
||||
session_data = {
|
||||
"user": {
|
||||
"name": "Jane Doe",
|
||||
"email": "jane@example.com",
|
||||
"preferred_username": "janedoe",
|
||||
}
|
||||
}
|
||||
cookie_val = signer.sign(b64encode(json.dumps(session_data).encode("utf-8"))).decode("utf-8")
|
||||
client_fresh.cookies.set("session", cookie_val)
|
||||
|
||||
resp = client_fresh.get("/help")
|
||||
assert resp.status_code == 200
|
||||
Reference in New Issue
Block a user